首頁 > 軟體

TCP程式設計詳解

2021-02-19 09:30:19


TCP把連線作為最基本的物件,每一條TCP連線都有兩個端點,這種端點我們稱為通訊端(socket)。埠號拼接到IP地址後面就構成了通訊端,例如192.0.0.32:80。IP協定雖然能把資料包文送到目的主機,但是並沒有交付給主機的具體應用程序,而端到端的通訊才是應用程序之間的通訊。

封包格式

TCP報文是TCP層傳輸的資料單元,也叫報文段。TCP報文的格式如下圖:

TCP報文中的標誌位:

  1. URG:緊急指標標誌,為1時表示緊機指標有效,為0時則忽略緊急指標
  2. ACK: 確認序號標誌。為1時表示確認號有效,為0時表示報文中不含確認資訊,忽略確認號欄位。
  3. PSH: push標誌,為1表示是帶有push標誌的資料,指示接收方在接收到該報文段以後,應儘快將這個報文段交給應用程式,而不是在緩衝區排隊
  4. RST: 重置連線標誌,用於重置由於主機崩潰或其他原因而出現錯誤的連線。或者用於拒絕非法的報文段和拒絕連線請求。
  5. SYN: 同步序號,用於建立連線過程,在連線請求中,SYN=1和ACK=0表示該資料段沒有使用捎帶的確認域,而連線應答捎帶一個確認,即SYN=1和ACK=1
  6. FIN: finish標誌,用於釋放連線,為1時表示傳送方已經沒有資料傳送了,即關閉本方資料流

建立連線(三次握手)

TCP通訊時的建立連線需要三次握手,意思是建立連線的時候,使用者端與伺服器之間需要三次封包的交流。

  1. 使用者端傳送給伺服器一個請求連線封包,即傳送了一個指向伺服器目標埠的一個SYN位為1的TCP報文
  2. 伺服器接收到使用者端的連線請求之後,會迴應一個SYN位為1的TCP報文,表示同意連線。並且會把ACK位也置1表示確認收到上次訊息
  3. 使用者端收到伺服器的同意連線的封包之後,還要回復一個ACK位1的TCP報文,表示確認收到

資料傳輸

TCP是以段為單位傳送資料的,在建立TCP連線的同時,每個封包的長度也被確定下來,一般稱其為最大訊息長度(MSS: Maximum Segment Size)。TCP在傳輸大量資料時,是以MSS的大小將資料進行分割傳送的,進行重發時也是以MSS為單位。兩端的主機在發出建立連線的請求時,會在TCP首部中寫入MSS選項,告訴對方自己的介面能夠適應的MSS的大小。為附加MSS選項,TCP的首部將不再是20位元組,而是4位元組的整數倍。會在兩者之間選擇一個較小的值投入使用。

斷開連線(四次揮手)

四次揮手,意思就是釋放連線的時候使用者端與伺服器之間需要四次封包的交流。

  1. 使用者端傳送給伺服器一個請求釋放連線的封包,即傳送了一個指向伺服器目標埠的一個FIN位為1的TCP報文,表示使用者端沒有資料要傳送了,但是仍然可以接收資料。並且ACK位也為1,表示對上次傳輸資料結果的確認。並且之後處於等待狀態,等待伺服器的兩次迴應
  2. 伺服器接收到使用者端的釋放連線請求之後,會先回應一個ACK位為1的報文,表示確認收到。但是這時伺服器可能還有資料沒有傳送完成,繼續傳送資料
  3. 伺服器傳送完資料之後,傳送一個FIN為1的TCP報文,表示我也沒有要傳送的資料了,你可以釋放連線了,當然ACK位仍為1
  4. 使用者端接受到伺服器的同意釋放連線的封包之後,回覆一個ACK為1的TCP報文,表示資料收到

基礎

socket程式設計一般採用使用者端-伺服器模式(即由客戶程序向伺服器程序發出請求,伺服器程序執行請求的任務並將執行結果返回給客戶程序的模式)

TCP使用者端流程

TCP使用者端socket程式設計流程

  1. 建立socket:Socket()
  2. 建立連線:Connect()
  3. 通訊:Send()Recv()
  4. 關閉socket: CloseSocket()

TCP使用者端編碼

  1. hostent結構體
hostent結構體

The hostent structure is defined in <netdb.h> as follows:

struct hostent {
   char  *h_name;            /* official name of host */
   char **h_aliases;         /* alias list */
   int    h_addrtype;        /* host address type */
   int    h_length;          /* length of address */
   char **h_addr_list;       /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */

The members of the hostent structure are:

h_name 
  The official name of the host.

h_aliases
  An array of alternative names for the host, terminated by a NULL pointer.

h_addrtype
  The type of address; always AF_INET or AF_INET6 at present.

h_length
  The length of the address in bytes.

h_addr_list
  An array of pointers to network addresses for the host (in network byte order), terminated by a NULL pointer.

h_addr The first address in h_addr_list for backward compatibility.
  1. gethostbyname & gethostbyaddr 過時了,應用應該使用getaddrinfo和getnameinfo.TCP使用者端
static const char send_data[] = "This is TCP Client from RT-Thread."; /* 傳送用到的資料 */
void tcpclient(int argc, char **argv)
{
    int ret;
    char *recv_data;
    struct hostent *host;
    int sock, bytes_received;
    struct sockaddr_in server_addr;
    const char *url;
    int port;

    if (argc < 3)
    {
        rt_kprintf("Usage: tcpclient URL PORTn");
        rt_kprintf("Like: tcpclient 192.168.12.44 5000n");
        return ;
    }

    url = argv[1];
    port = strtoul(argv[2], 0, 10);

    /* 通過函數入口引數url獲得host地址(如果是域名,會做域名解析) */
    host = gethostbyname(url);

    /* 分配用於存放接收資料的緩衝 */
    recv_data = rt_malloc(BUFSZ);
    if (recv_data == RT_NULL)
    {
        rt_kprintf("No memoryn");
        return;
    }

    /* 建立一個socket,型別是SOCKET_STREAM,TCP型別 */
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        /* 建立socket失敗 */
        rt_kprintf("Socket errorn");

        /* 釋放接收緩衝 */
        rt_free(recv_data);
        return;
    }

    /* 初始化預連線的伺服器端地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr = *((struct in_addr *)host->h_addr);
    rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));

    /* 連線到伺服器端 */
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
    {
        /* 連線失敗 */
        rt_kprintf("Connect fail!n");
        closesocket(sock);

        /*釋放接收緩衝 */
        rt_free(recv_data);
        return;
    }
    else
    {
        /* 連線成功 */
        rt_kprintf("Connect successfuln");
    }

    while (1)
    {
        /* 從sock連線中接收最大BUFSZ - 1位元組資料 */
        bytes_received = recv(sock, recv_data, BUFSZ - 1, 0);
        if (bytes_received < 0)
        {
            /* 接收失敗,關閉這個連線 */
            closesocket(sock);
            rt_kprintf("nreceived error,close the socket.rn");

            /* 釋放接收緩衝 */
            rt_free(recv_data);
            break;
        }
        else if (bytes_received == 0)
        {
            /* 預設 recv 為阻塞模式,此時收到0認為連線出錯,關閉這個連線 */
            closesocket(sock);
            rt_kprintf("nreceived error,close the socket.rn");

            /* 釋放接收緩衝 */
            rt_free(recv_data);
            break;
        }

        /* 有接收到資料,把末端清零 */
        recv_data[bytes_received] = '';

        if (strncmp(recv_data, "q", 1) == 0 || strncmp(recv_data, "Q", 1) == 0)
        {
            /* 如果是首字母是q或Q,關閉這個連線 */
            closesocket(sock);
            rt_kprintf("n got a 'q' or 'Q',close the socket.rn");

            /* 釋放接收緩衝 */
            rt_free(recv_data);
            break;
        }
        else
        {
            /* 在控制終端顯示收到的資料 */
            rt_kprintf("nReceived data = %s ", recv_data);
        }

        /* 傳送資料到sock連線 */
        ret = send(sock, send_data, strlen(send_data), 0);
        if (ret < 0)
        {
            /* 接收失敗,關閉這個連線 */
            closesocket(sock);
            rt_kprintf("nsend error,close the socket.rn");

            rt_free(recv_data);
            break;
        }
        else if (ret == 0)
        {
            /* 列印send函數返回值為0的警告資訊 */
            rt_kprintf("n Send warning,send function return 0.rn");
        }
    }
    return;
}

TCP伺服器端流程

  1. 建立socket:socket()
  2. 將建立的socket繫結到一個IP地址和埠號上:bind()
  3. 設定socket為監聽模式:listen()
  4. 接受請求並返回socket:accept()。accept會阻塞住等待接收訊息
  5. 與使用者端進行通訊recv() & send()
  6. 關閉socket:close()

TCP伺服器端編碼

static void tcpserv(int argc, char **argv)
{
    char *recv_data; /* 用於接收的指標,後面會做一次動態分配以請求可用記憶體 */
    socklen_t sin_size;
    int sock, connected, bytes_received;
    struct sockaddr_in server_addr, client_addr;
    rt_bool_t stop = RT_FALSE; /* 停止標誌 */
    int ret;

    recv_data = rt_malloc(BUFSZ + 1); /* 分配接收用的資料緩衝 */
    if (recv_data == RT_NULL)
    {
        rt_kprintf("No memoryn");
        return;
    }

    /* 一個socket在使用前,需要預先建立出來,指定SOCK_STREAM為TCP的socket */
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        /* 建立失敗的錯誤處理 */
        rt_kprintf("Socket errorn");

        /* 釋放已分配的接收緩衝 */
        rt_free(recv_data);
        return;
    }

    /* 初始化伺服器端地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(5000); /* 伺服器端工作的埠 */
    server_addr.sin_addr.s_addr = INADDR_ANY;
    rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));

    /* 繫結socket到伺服器端地址 */
    if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
    {
        /* 繫結失敗 */
        rt_kprintf("Unable to bindn");

        /* 釋放已分配的接收緩衝 */
        rt_free(recv_data);
        return;
    }

    /* 在socket上進行監聽 */
    if (listen(sock, 5) == -1)
    {
        rt_kprintf("Listen errorn");

        /* release recv buffer */
        rt_free(recv_data);
        return;
    }

    rt_kprintf("nTCPServer Waiting for client on port 5000...n");
    while (stop != RT_TRUE)
    {
        sin_size = sizeof(struct sockaddr_in);

        /* 接受一個使用者端連線socket的請求,這個函數呼叫是阻塞式的 */
        connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);
        /* 返回的是連線成功的socket */
        if (connected < 0)
        {
            rt_kprintf("accept connection failed! errno = %dn", errno);
            continue;
        }

        /* 接受返回的client_addr指向了使用者端的地址資訊 */
        rt_kprintf("I got a connection from (%s , %d)n",
                   inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        /* 使用者端連線的處理 */
        while (1)
        {
            /* 傳送資料到connected socket */
            ret = send(connected, send_data, strlen(send_data), 0);
            if (ret < 0)
            {
                /* 傳送失敗,關閉這個連線 */
                closesocket(connected);
                rt_kprintf("nsend error,close the socket.rn");
                break;
            }
            else if (ret == 0)
            {
                /* 列印send函數返回值為0的警告資訊 */
                rt_kprintf("n Send warning,send function return 0.rn");
            }

            /* 從connected socket中接收資料,接收buffer是1024大小,但並不一定能夠收到1024大小的資料 */
            bytes_received = recv(connected, recv_data, BUFSZ, 0);
            if (bytes_received < 0)
            {
                /* 接收失敗,關閉這個connected socket */
                closesocket(connected);
                break;
            }
            else if (bytes_received == 0)
            {
                /* 列印recv函數返回值為0的警告資訊 */
                rt_kprintf("nReceived warning,recv function return 0.rn");
                closesocket(connected);
                break;
            }

            /* 有接收到資料,把末端清零 */
            recv_data[bytes_received] = '';
            if (strcmp(recv_data, "q") == 0 || strcmp(recv_data, "Q") == 0)
            {
                /* 如果是首字母是q或Q,關閉這個連線 */
                closesocket(connected);
                break;
            }
            else if (strcmp(recv_data, "exit") == 0)
            {
                /* 如果接收的是exit,則關閉整個伺服器端 */
                closesocket(connected);
                stop = RT_TRUE;
                break;
            }
            else
            {
                /* 在控制終端顯示收到的資料 */
                rt_kprintf("RECEIVED DATA = %s n", recv_data);
            }
        }
    }

    /* 退出服務 */
    closesocket(sock);

    /* 釋放接收緩衝 */
    rt_free(recv_data);

    return ;
}

參考文獻

  1. RT-Thread視訊中心核心入門
  2. RT-Thread檔案中心

本文作者: CrazyCatJack

本文連結: https://www.cnblogs.com/CrazyCatJack/p/14408903.html

版權宣告:本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!

關注博主:如果您覺得該文章對您有幫助,可以點選文章右下角推薦一下,您的支援將成為我最大的動力!



IT145.com E-mail:sddin#qq.com