TCP編程入門簡介

在前幾篇文章中,我們先從宏觀角度(TCP 概述)大致介紹了 tcp 的概念,然后從微觀角度(滑動窗口、擁塞窗口等)詳細說明了從 client 端和 server 端,tcp 是如何進行網絡控制的。在本文中,我們將通過一個 tcp 例子,將整個過程聯(lián)通起來,講解 tcp 從連接、發(fā)送以及關閉,整個流程是怎樣運行的。
示例代碼
server.c
#include?
#include?
#include?
#include?
#include?
#include?
int?main(int?argc,?char?**)?{
????int?server_fd,?new_socket,?valread;
????struct?sockaddr_in?address;
????int?opt?=?1;
????int?addrlen?=?sizeof(address);
????char?buffer[1024]?=?{0};
????char?*hello?=?"hello?from?server";
??? int port = 8080;
????//?創(chuàng)建socket文件描述符
????if?((server_fd?=?socket(AF_INET,?SOCK_STREAM,?0))?==?0)?{
????????return?1;
????}
?????//?端口?&?地址復用
????if?(setsockopt(server_fd,?SOL_SOCKET,?SO_REUSEADDR?|?SO_REUSEPORT,
??????????????????????????????????????????????????&opt,?sizeof(opt)))?{
????????return?1;
????}
????address.sin_family?=?AF_INET;
????address.sin_addr.s_addr?=?INADDR_ANY;
????address.sin_port?=?htons(?port?);
????//?綁定8080端口
????if?(bind(server_fd,?(struct?sockaddr?*)&address,
?????????????????????????????????sizeof(address))<0)?{
????????perror("bind?failed");
????????return?1;
????}
????if?(listen(server_fd,?3)?0)?{
????????return?1;
????}
????if?((new_socket?=?accept(server_fd,?(struct?sockaddr?*)&address,
???????????????????????(socklen_t*)&addrlen))<0)?{
????????return?1;
????}
????valread?=?read(?new_socket?,?buffer,?1024);
????//?向client發(fā)送消息
????send(new_socket?,?hello?,?strlen(hello)?,?0?);
????close(new_socket);
????close(server_fd);
????return?0;
}
client.c
#include?
#include?
#include?
#include?
#include?
int?main(int?argc,?char?const?*argv[])?{
????int?sock?=?0,?valread;
????struct?sockaddr_in?serv_addr;
????char?*hello?=?"hello?from?client";
????char?buffer[1024]?=?{0};
????int?port?=?8080;
????if?((sock?=?socket(AF_INET,?SOCK_STREAM,?0))?0)?{????????return?-1;
????}
????serv_addr.sin_family?=?AF_INET;
????serv_addr.sin_port?=?htons(port);
????if(inet_pton(AF_INET,?"127.0.0.1",?&serv_addr.sin_addr)<=0)?{
????????return?1;
????}
????//?建立連接
????if?(connect(sock,?(struct?sockaddr?*)&serv_addr,?sizeof(serv_addr))?0)?{
????????return?1;
????}
????send(sock?,?hello?,?strlen(hello)?,?0?);
????valread?=?read(?sock?,?buffer,?1024);
????close(sock);
????return?0;
}
上面是 tcp 的示例代碼,很簡單,算是網絡編程的入門級代碼。讀者可以使用下面命令進行編譯:
gcc?-g?server.c?-o?server
gcc?-g?client.c?-o?client
過程分析
下面,我們分析前面的示例代碼, 對于 server 端:
1、創(chuàng)建 socket
2、bind 端口
3、listen
4、accept 新的連接,獲取新連接的 socket
5、通過 read 函數(shù),接收數(shù)據(jù)
6、通過 send 函數(shù),發(fā)送數(shù)據(jù)
7、調用 close 函數(shù)關閉 socket
對于 client 端:
1、創(chuàng)建 socket
2、通過 connect 與 server 端建立連接
3、通過 send 發(fā)送數(shù)據(jù)
4、通過 recv 接收數(shù)據(jù)
5、close socket
對于 server 端和 client 端的每個步驟,其都是有關聯(lián)的,比如 client 端調用 connect 之后,server 端將從 accept 函數(shù)返回,其返回值是新連接的 socket 等等。下面,我們將以一個圖的方式來了解兩端的聯(lián)系。

詳述
socket()
#include?
int?socket?(int?domain,?int?type,?int?protocol);
socket 系統(tǒng)調用通過分配一個新的描述符來創(chuàng)建一個新的套接字。它唯一標識一個 socket。這個 socket 描述字跟文件描述字一樣,后續(xù)的操作都有用到它,把它作為參數(shù),通過它來進行一些讀寫操作。成功時返回一個非負的文件描述符編號,錯誤時返回-1。
domain:即協(xié)議域,又稱為協(xié)議族(family)。常用的協(xié)議族有,AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或稱 AF_UNIX,Unix 域 socket)、AF_ROUTE 等等。 type:指定 socket 類型。常用的 socket 類型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(數(shù)據(jù)報式套接字)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET 等等 protocol:就是指定協(xié)議。常用的協(xié)議有,IPPROTO_TCP、PPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC 等,它們分別對應 TCP 傳輸協(xié)議、UDP 傳輸協(xié)議、STCP 傳輸協(xié)議、TIPC 傳輸協(xié)議。
在此,需要注意的是,并不是上面的 type 和 protocol 可以隨意組合的,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 組合。當 protocol 為 0 時,會自動選擇 type 類型對應的默認協(xié)議。由于數(shù)據(jù)流的 IP 協(xié)議就是 TCP,前兩個參數(shù)已經有效地指定了 TCP。因此,第三個參數(shù)可以保留為 0,讓操作系統(tǒng)分配一個默認協(xié)議(這將是 IPPROTO_TCP)。
bind()
?#include???????????/*?See?NOTES?*/
?#include?
?int?bind(int?sockfd,?const?struct?sockaddr?*addr,?socklen_t?addrlen);
bind 函數(shù)用來綁定 socket 的本地地址和端口號。
sockfd 是待綁定的 socket 對應的文件描述符。 addr 一個 const struct sockaddr *指針,指向要綁定給 sockfd 的協(xié)議地址。 addrlen 必須是結構體的大小 sockaddr_in(或正在使用的任何結構)。
struct?in_addr?{
u_int32_t?s_addr;
};
struct?sockaddr_in?{
short?sin_family;
u_short?sin_port;
struct?in_addr?sin_addr;
char?sin_zero[8];
};
sin_port 指定要使用的 16 位端口號。它在網絡中給出(大端)字節(jié)順序,因此必須使用 htons 將主機字節(jié)順序轉換為網絡字節(jié)順序。
將 sin 端口值指定為 0 會告訴操作系統(tǒng)選擇端口號。操作系統(tǒng)將在 1024 和 5000 之間選擇一個未使用的端口號作為應用程序。注意只有超級用戶才能綁定 1024 以下的端口號。
對于 bind 系統(tǒng)調用,此處需要注意的是,在 client 不需要強制調用,而在 server 端需要強制調用,這是因為,在 server 端,bind 之后,要進行端口監(jiān)聽。而在 client 端,如果代碼中沒有執(zhí)行此系統(tǒng)調用,則在內核中會隱式執(zhí)行此調用。
connect()
#include???????????/*?See?NOTES?*/
#include?
int?connect(int?sockfd,?const?struct?sockaddr?*addr,?socklen_t?addrlen);
通過執(zhí)行 connect 函數(shù),與 server 端建立連接。
sockfd socket 對應的文件描述符。 addr 指定了對端的 ip 和端口。 addrlen 指定了上面結構體的大小
經典的三次握手,就在 connect 階段,如下圖:
listen()
?#include???????????/*?See?NOTES?*/
?#include?
?int?listen(int?sockfd,?int?backlog);
listen() 函數(shù)的主要作用就是將套接字( sockfd )變成被動的連接監(jiān)聽套接字(被動等待客戶端的連接),至于參數(shù) backlog 的作用是設置內核中連接隊列的長度(該參數(shù)在現(xiàn)在大部分系統(tǒng)中已經不被使用),listen()的作用僅僅告訴內核一些信息。成功返回 0,否則返回-1.
sockfd 是綁定到要接受的端口的未連接套接字連接。 backlog 以前指定了操作系統(tǒng)在應用程序之前接受的連接數(shù)。現(xiàn)在這個值已經沒用了。
這里需要注意的是,listen()函數(shù)不會阻塞,它主要做的事情為,將該套接字和套接字對應的連接隊列長度告訴 Linux 內核,然后,listen()函數(shù)就結束。
accept()
#include???????????/*?See?NOTES?*/
#include?
int?accept(int?sockfd,?struct?sockaddr?*addr,?socklen_t?*addrlen);
accept()函數(shù)功能是,從處于 established 狀態(tài)的連接隊列頭部取出一個已經完成的連接,如果這個隊列沒有已經完成的連接,accept()函數(shù)就會阻塞,直到取出隊列中已完成的用戶連接為止。如果沒有客戶端連接,accept 將阻塞直到一個 做。錯誤返回 -1。
sockfd 指的是上面綁定以及 listen 的 socket 描述符 addr 在有連接過來的時候,里面的內容就是 client 端的信息 addrlen 上一個變量的長度
需要注意的是,accept 的功能并不是建立連接,而是從當前連接的等待隊列中獲取一條連接。
accept 的第一個參數(shù)為服務器的 socket 描述字,是服務器開始調用 socket()函數(shù)生成的,稱為監(jiān)聽 socket 描述字;而 accept 函數(shù)返回的是已連接的 socket 描述字。一個服務器通常通常僅僅只創(chuàng)建一個監(jiān)聽 socket 描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創(chuàng)建了一個已連接 socket 描述字,當服務器完成了對某個客戶的服務,相應的已連接 socket 描述字就被關閉。
send()
#include?
#include?
ssize_t?send(int?sockfd,?const?void?*buf,?size_t?len,?int?flags);
向指定 socket 發(fā)送數(shù)據(jù)。
sockfd 指定 socket 描述符 buf 待發(fā)送的數(shù)據(jù) buf len 數(shù)據(jù) buf 長度 flags 發(fā)送標記。其值為 MSG_CONFIRM MSG_DONTROUTE MSG_DONTWAIT MSG_EOR MSG_MORE MSG_NOSIGNAL MSG_OOB 中 1 個或者幾個的邏輯或或者邏輯與值
對于 send()的詳細講解,請轉閱TCP之send&recv
recv()
#include?
#include?
ssize_t?recv(int?sockfd,?void?*buf,?size_t?len,?int?flags);
從指定socket中讀取數(shù)據(jù)。
sockfd 指定 socket 描述符 buf 接收數(shù)據(jù) buf len 接收數(shù)據(jù)buf 長度 flags 發(fā)送標記。其值為 MSG_CMSG_CLOEXEC MSG_DONTWAIT MSG_ERRQUEUE MSG_OOB MSG_PEEK MSG_TRUNC MSG_WAITALL 中 1 個或者幾個的邏輯或或者邏輯與值
對于 recv()的詳細講解,請轉閱TCP之send&recv
close()
?#include?
?int?close(int?fd);
關閉socket。
fd為待關閉的文件描述符
close 一個套接字的默認行為是把套接字標記為已關閉,然后立即返回到調用進程,該套接字描述符不能再由調用進程使用,也就是說它不能再作為send或recv的第一個參數(shù),然而TCP將嘗試發(fā)送已排隊等待發(fā)送到對端,發(fā)送完畢后發(fā)生的是正常的TCP連接終止序列。在多進程并發(fā)服務器中,父子進程共享著套接字,套接字描述符引用計數(shù)記錄著共享著的進程個數(shù),當父進程或某一子進程close掉套接字時,描述符引用計數(shù)會相應的減一,當引用計數(shù)仍大于零時,這個close調用就不會引發(fā)TCP的四路握手斷連過程。四次揮手就在這個步驟。
END
