【網絡】(三)流協議粘包問題

  在第二篇中,談到了流協議的粘包問題以及一種解決方法的實現,在數據的開始顯示的指明數據的長度;這是一種不錯的解決方法,另外,也有說到可以根據數據中的特殊字符來判斷是否接收完了一整條消息,本篇將在上一篇的基礎上實現這一方法!

原理:依據數據尾部是否有’\n’,來表示消息是否接收完成

 
1、核心函數實現

//使用recv函數從套接字接收緩衝區中接收數據,但並不從緩衝去中清除數據
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while(true)
    {
        int iret = recv(sockfd, buf, len, MSG_PEEK);
        if(iret == -1 && errno == EINTR)    //如果失敗是因爲信號中端,那麼就重新再試
            continue;
        return iret;
    }
}

注意:
  這裏使用了recv函數,它和read函數的一個不同在於,它最後一個參數,可以指定一個選項,表示從接收緩衝區中讀取數據後,該數據是否被移出消息隊列,MSG_PEEK標誌表示不移出數據而偷窺數據!

//如果讀到的數據包含\n,則返回,表示一條完整的消息讀取完畢
ssize_t recvline(int sockfd, void *buf, size_t maxlen)
{
    int iret = 0;
    int nread = 0;  //已讀數據字節數
    char *pbuf = (char*)buf;
    int nleft = maxlen; //剩餘數據字節數
    while(true)
    {
        iret = recv_peek(sockfd, pbuf, nleft);
        if(iret < 0)
            return iret;    //讀取失敗
        else if(iret == 0)
            return iret;    //對方關閉套接口

        nread = iret;
        if(nread > nleft)       //已經讀取到的數據只可能小於或者等於剩餘的數據
            exit(EXIT_FAILURE);

        for(int i = 0; i < nread; i++)
        {
            if(pbuf[i] == '\n')
            {
                iret = readn(sockfd, pbuf, i+1);    //從緩衝區中讀走包括\n在內的數據
                if(iret != i+1)
                    exit(EXIT_FAILURE);     //沒有讀取都i+1個數據,說明失敗
                return iret;                //讀取都\n返回
            }
        }

        //在當前讀到的數據中沒有發現\n,那麼先將這部分數據從緩衝區中讀走,然後接着偷窺後面的數據
        nleft -= nread;
        iret = readn(sockfd, pbuf, nread);
        if(iret != nread)
            exit(EXIT_FAILURE);
        pbuf += nread;
    }
    return -1;
}

readn和writen函數參照第二篇文章!

 
2、服務器代碼

編譯命令:gcc -Wall -g -std=gnu99 server.c -o server

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>


#define handle_error(msg)   \
                    do{perror(msg);exit(EXIT_FAILURE);}while(0)

//忽略子進程的SIG_CHLD信號
void handle_SIGIGN(void)
{
  struct sigaction act_chld;
  act_chld.sa_handler = SIG_IGN;
  act_chld.sa_flags = 0;
  sigemptyset(&act_chld.sa_mask);

  if(-1 == sigaction(SIGCHLD, &act_chld, NULL))//捕獲終端中斷信號
      handle_error("sigaction SIGCHLD");
}

void do_work(int sock);


int main(void)
{
    handle_SIGIGN();

    int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    if(sk_fd < 0)
        handle_error("socket");


    //使用REUSEADDR,不必等待TIME_WAIT 狀態消失,就可以重新使用端口
    int on = 1;
    if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    {
        close(sk_fd);
        handle_error("setsockopt");
    }


    struct sockaddr_in sr_addr;
    memset(&sr_addr,0,sizeof(sr_addr));
    sr_addr.sin_family = AF_INET;
    sr_addr.sin_port = htons(5188);
    sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //inet_aton("127.0.0.1",&sr_addr.sin_addr);

    if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
    {
        close(sk_fd);
        handle_error("bind");
    }

    //被動套接字
    if(listen(sk_fd, SOMAXCONN) < 0)     //內核爲此套接字排隊的最大連接數由SOMAXCONN宏指定
    {
        close(sk_fd);
        handle_error("listen");
    }


    struct sockaddr_in cl_addr;
    socklen_t cl_length = sizeof(cl_addr);
    pid_t pid;
    while (true)
    {
        memset(&cl_addr,0,sizeof(cl_addr));
        int ac_sk = accept(sk_fd, (struct sockaddr *)&cl_addr, &cl_length);
        if(ac_sk < 0)
        {
            if(errno == EINTR)
                continue;
            close(sk_fd);
            handle_error("accept");
        }
        printf("Connect ip = %s\tport = %d\n",inet_ntoa(cl_addr.sin_addr),ntohs(cl_addr.sin_port));

        //每個客戶端對應一個子進程
        pid = fork();
        if(pid == -1)
            handle_error("fork");
        if(pid == 0)
        {
            close(sk_fd);
            do_work(ac_sk);
            close(ac_sk);
            exit(EXIT_SUCCESS);
        }
        else
            close(ac_sk);
    }
    close(sk_fd);
    return 0;
}


void do_work(int sock)
{
    char recvbuf[1024];
    while(true)
    {
        memset(recvbuf,0,sizeof(recvbuf));
        int iret = recvline(sock,recvbuf,sizeof(recvbuf));    //獲取包數據長度
        if(iret == -1)
            handle_error("read");
        else if(iret == 0)
        {
            printf("Client was closed!\n");
            break;
        }
        fputs(recvbuf,stdout);
        writen(sock, recvbuf, strlen(recvbuf));    //回傳數據
    }
}

 
3、客戶端代碼

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>


#define handle_error(msg)   \
                    do{perror(msg);exit(EXIT_FAILURE);}while(0)


int main(void)
{
    int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    if(sk_fd < 0)
        handle_error("socket");
    struct sockaddr_in sr_addr;
    memset(&sr_addr,0,sizeof(sr_addr));
    sr_addr.sin_family = AF_INET;
    sr_addr.sin_port = htons(5188);
    //sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //inet_aton("127.0.0.1",&sr_addr.sin_addr);

    if(connect(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
    {
        close(sk_fd);
        handle_error("connect");
    }

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        writen(sk_fd, sendbuf, strlen(sendbuf));   //發送數據

        int iret = recvline(sk_fd, recvbuf, sizeof(recvbuf)); //接收包數據長度
        if(iret == -1)
            handle_error("read");
        else if(iret == 0)
        {
            printf("Server was closed!\n");
            break;
        }
        fputs(recvbuf, stdout);
        memset(&sendbuf, 0, sizeof(sendbuf));
        memset(&recvbuf, 0, sizeof(recvbuf));
    }
    close(sk_fd);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章