1. 程式人生 > 實用技巧 >計算機通訊之謎,帶你徹底理解socket網路程式設計(二)

計算機通訊之謎,帶你徹底理解socket網路程式設計(二)

在《計算機通訊之謎,帶你徹底理解socket網路程式設計(一)》一文裡我們提到了,客戶端傳送了資料了之後,不管服務端還是客戶端都close退出了,也就是說只能傳送一次資料,這顯然不符合實際的用途。那麼該如何更改程式呢?


1、持續傳送

要想實現持續傳送,聰明的你可能想到了用迴圈,思路完全正確,但是迴圈多少次呢?實際的使用情況服務端一直都要執行,除非系統崩掉了,而客戶端和服務端的長連線也要一直連著,除非客戶端自己關閉了連線。所以我們的思路是雙端都無限迴圈!所以可以用一個while(1)來迴圈,那麼應該在哪裡開始迴圈,這個要看自己具體的要求,如果要求三次握手只一次,隨後傳送資料的話進行持續傳送。那麼服務端和客戶端的程式碼分別如下:

1.1、服務端程式碼

 

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } 
while (0) int main() { //建立套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //初始化socket元素 struct sockaddr_in server_addr; int server_len = sizeof(server_addr); memset(&server_addr, 0, server_len); server_addr.sin_family
= AF_INET; //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用這個寫法也可以 server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(39002); //繫結檔案描述符和伺服器的ip和埠號 int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len); if (m_bindfd < 0) { ERR_EXIT("bind ip and port fail"); } //進入監聽狀態,等待使用者發起請求 int m_listenfd = listen(m_sockfd, 20); if (m_listenfd < 0) { ERR_EXIT("listen client fail"); } //定義客戶端的套接字,這裡返回一個新的套接字,後面通訊時,就用這個m_connfd進行通訊 struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len); printf("client accept success\n"); //接收客戶端資料,並相應 char buffer[BUF_SIZE]; while (1) { memset(buffer, 0, sizeof(buffer)); //重置緩衝區 recv(m_connfd, buffer, sizeof(buffer)-1, 0); printf("server recv:%s\n", buffer); strcat(buffer, "+ACK"); send(m_connfd, buffer, sizeof(buffer)-1, 0); } //關閉套接字 close(m_connfd); close(m_sockfd); printf("server socket closed!!!\n"); return 0; }

 

1.2、客戶端程式碼

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
 
int main()
{
    //建立套接字
    int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (m_sockfd < 0)
    {
        ERR_EXIT("create socket fail");
    }
 
    //伺服器的ip為本地,埠號
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
    server_addr.sin_port = htons(39002);
 
    //向伺服器傳送連線請求
    int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (m_connectfd < 0)
    {
        ERR_EXIT("connect server fail");
    }
 
    //傳送並接收資料
    char buffer[BUF_SIZE];
    while (1)
    {
        memset(buffer, 0, sizeof(buffer)); //重置緩衝區
        printf("client send:");
        scanf("%s", buffer);
        send(m_sockfd, buffer, sizeof(buffer)-1, 0);
        recv(m_sockfd, buffer, sizeof(buffer)-1, 0);
        printf("client recv:%s\n", buffer);
    }
 
    //斷開連線
    close(m_sockfd);
 
    printf("client socket closed!!!\n");
 
    return 0;
}

以上程式碼就可以正確的持續傳送資料,程式碼已經在Linux主機上執行過,完全正確,如果有問題,歡迎評論說明。需要的童鞋可以拿去用。

2、服務端一直收到空包

那麼以上程式碼有沒有什麼問題呢?如果親自在linux主機上執行過的小夥伴可能會發現如果用Ctrl+C掐掉客戶端或者kill掉客戶端程序的話,服務端會不停的列印收到的資料,但是資料卻是空字串,GDB除錯發現收到的實際上的0長度的空包(有時候持續收到多個空包後服務端就會斷開連線)。也就是說

當客戶端斷開,服務端不停的接收到一個0位元組

這個非常奇怪,客戶端已經斷開了,為什麼服務端還會收到一個0位元組的資料呢?這個問題我找了很久依舊沒有找到一個合理的答案,網上的解釋不是很明白,有懂的同學可以在評論區幫忙解釋下。以下解釋來自網路

一、使用者正常退出,這時你還迴圈呼叫了Receive方法時會出現,並會陷入死迴圈。

二、使用者非正常退出,包括拔網線,宕機,伺服器會捕捉到一個異常

問題的原因暫時無法找到,但是解決的辦法卻是有的,我們在收到客戶端的資料的時候可以判斷資料包的大小,如果為0的可能就是表示關閉客戶端了。那麼這時候服務端要做的就是判斷收到的資料包的大小,如果是0的話就去close()客戶端的連線,這樣子就可以正常的接受和傳送資料,客戶端關閉了,服務端也不會有問題。修改後的while部分的程式碼如下

while (1)
    {
        if (m_connfd < 0)
        {
            m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
            printf("client accept success again!!!\n");
        }
        memset(buffer, 0, sizeof(buffer)); //重置緩衝區
        int recvLen = recv(m_connfd, buffer, sizeof(buffer) - 1, 0);
        if (recvLen <= 0)
        {
            close(m_connfd);
            m_connfd = -1;
            printf("client lose connection!!!\n");
            continue;
        }
        printf("server recv:%s\n", buffer);
        strcat(buffer, "+ACK");
        send(m_connfd, buffer, sizeof(buffer) - 1, 0);
    }

3、程式碼缺陷,問題思考

以上的程式碼確實可以實現客戶端持續傳送資料,客戶端斷開與重連,服務端仍舊保持正常的過程。但是如果你以為socket網路程式設計就這麼簡單的話,那麼你就真的太天真了。以上的客戶端和服務端僅僅只是一對一的,就是說服務端就一個程序,客戶端也就一個程序。但實際應用的過程中怎麼可能這麼簡單呢?服務端可能就一臺,但是客戶端是多臺的。

那這時候請同學動手做個實驗,在啟動一個客戶端程序,你會發現服務端沒反應,客戶端傳送資料的話,服務端也是沒反應!那麼該怎麼辦呢?聰明的你能夠想到解決辦法嗎?欲知後事如何,請聽下回分解!

更多精彩內容,請關注同名公眾:一點筆記alittle