
系列文章目錄
文章目錄
- 系列文章目錄
- 前言
- 一、網路資料的五元組資訊
- 1.理解源IP地址和目的IP地址
- 2.理解 "埠號" 和 "行程ID"
- 3.理解源埠號和目的埠號
- 4.理解TCP協議
- 5.理解UDP協議
- 二、主機位元組序<===>網路位元組序
- 三、點分十進制IP<===>uint32_t
- 四、UDP的socket編程(流程&介面)
- 1.UDP的socket編程流程
- 1.socket常見API
- 2.socketaddr結構的分類
- 3.socketaddr結構
- 4.socketaddr_in結構
- 5.in_addr結構
- 2.UDP的socket編程介面
- 1.創建套接字socket介面
- 2.系結埠號bind介面
- 3.UDP發送介面sendto
- 4.UDP接收介面recvform
- 5.UDP關閉介面close
- 3.客戶端為什么不推薦系結地址資訊
- 五、UDP的socket編程代碼
- 1.客戶端
- 2.服務端
- 3.查看埠的使用情況:netstat -anp | grep [埠號]
- 六、TCP的socket編程(流程&介面)
- 1.TCP的socket編程流程
- 2.TCP的socket編程介面
- 1.服務端創建套接字socket介面
- 2.服務端系結套接字bind介面
- 3.服務端監聽套接字listen介面
- 4.服務端接收鏈接套接字accept介面
- 5.客戶端連接套接字connect介面
- 6.TCP發送介面send介面
- 7.TCP接收介面recv介面
- 8.TCP關閉介面close介面
- 3.TCP的連接建立
- 4.單行程的TCP的發送和接收資料
- 1.客戶端代碼
- 2.服務端端代碼
- 5.單行程的TCP的發送和接收資料的問題
- 6.多執行緒的TCP的發送和接收資料
- 1.客戶端代碼
- 2.服務端代碼
- 7. 多行程的TCP的發送和接收資料
- 1.客戶端代碼
- 2.服務端代碼
- 七、TCP協議通訊流程
- 💬總結
前言

一、網路資料的五元組資訊

1.理解源IP地址和目的IP地址

- 在IP資料包頭部中, 有兩個IP地址, 分別叫做源IP地址, 和目的IP地址:
- 源IP地址:表示該條資訊來源于哪個機器,
- 目的IP地址:表示該條資訊去往于哪個行程,
2.理解 “埠號” 和 “行程ID”
- 埠號(port)是傳輸層協議的內容:
- 埠號是一個2位元組16位的整數,
- 埠號用來標識一個行程, 告訴作業系統, 當前的這個資料要交給哪一個行程來處理,
- IP地址 + 埠號能夠標識網路上的某一臺主機的某一個行程,
- 一個埠號只能被一個行程占用,
- 一個行程可以系結多個埠號,
- pid 表示唯一一個行程; 此處我們的埠號也是唯一表示一個行程,
3.理解源埠號和目的埠號
- 傳輸層協議(TCP和UDP)的資料段中有兩個埠號, 分別叫做源埠號和目的埠號. 就是在描述 “資料是誰發的, 要發給誰”:
- 源埠號:表示該條資訊來源于哪個行程,
- 目的埠號:表示該條資訊去往于哪個機器,
以寄快遞為例子:

4.理解TCP協議

- 協議:兩臺機器傳輸時用哪種協議,
- TCP(Transmission Control Protocol 傳輸控制協議)有一個直觀的認識; 后面我們再詳細討論TCP的一些細節問題,
- 傳輸層協議,
- 有連接:雙方在發送網路資料之前必須建立連接,在進行發送,
- 可靠傳輸:保證資料是可靠并且有序的到達對端,例如發送123、456時123資料先到達,456資料后到達,但是有時可以456資料先到達傳輸層,但會阻塞等待先等前面的資料就是123先到達,
- 面向位元組流:TCP發送資料的單位是以位元組為單位,并且資料沒有明顯的邊界例如:123456資料不會分開,

假設應用層要想傳輸層傳入“hello”,當hello傳入傳輸層還尾傳入網路層時,應用層又想向傳輸層傳入“world”,此時是不能傳輸的,只有等“hello”從傳輸層傳入網路層,“world”才能從應用層傳入傳輸層:

5.理解UDP協議

- 此處我們也是對UDP(User Datagram Protocol 用戶資料報協議)有一個直觀的認識; 后面再詳細討論,
- 傳輸層協議,
- 無連接:雙方在發送網路資料之前不需要建立連接,直接發送,客服端不用管服務端是否在線,
- 可靠傳輸:UDP并不會保證資料有序的到達對端
- 面向位元組流:UDP不管向應用層還是網路層傳遞資料都是整條資料
假設A機器的應用層先向傳輸層傳入一個“aaa”,再向傳輸層傳入一個“bbb”,到待對端機器的傳輸層不會區分,是不是一次傳過來的:
、

二、主機位元組序<===>網路位元組序

- 我們已經知道,記憶體中的多位元組資料相對于記憶體地址有大端和小端之分, 磁盤檔案中的多位元組資料相對于檔案中的偏移地址也有大端小端之分, 網路資料流同樣有大端小端之分. 那么如何定義網路資料流的地址呢?
- 發送主機通常將發送緩沖區中的資料按記憶體地址從低到高的順序發出,
- 接收主機把從網路上接到的位元組依次保存在接識訓沖區中,也是按記憶體地址從低到高的順序保存,
- 網路資料流的地址應這樣規定:先發出的資料是低地址,后發出的資料是高地址,
- TCP/IP協議規定,網路資料流應采用大端位元組序,即低地址高位元組,
- 不管這臺主機是大端機還是小端機, 都會按照這個TCP/IP規定的網路位元組序來發送/接收資料,
- 如果當前發送主機是小端, 就需要先將資料轉成大端; 否則就忽略, 直接發送即可,

- 網路資料需要進行轉發之前:由主機位元組序轉換成為網路位元組序,
- 網路資料接收之前:由網路位元組序轉換成為主機位元組序,
- 【問題】為什么網路資料需要進行轉化成為網路位元組序?
- 網路規定采用大端位元組序作為網路位元組序,
- 路由設備或者交換機需要對網路資料進行分用到網路層面,以獲取到“目的IP地址”,而這些設備在進行分用的時候默認是按照網路位元組序進行分用的,
- 主機位元組序轉換為網路位元組序(host to network)
- 2個位元組 ?uint16_t htons(uint16_t hostshort),
- 4個位元組 ?uint32_t htonl(uint32_t hostlong),
- 網路位元組序轉換為主機位元組序( to network)
- 2個位元組 ?uint16_t ntohs(uint16_t netshort);
- 4個位元組 ?uint32_t ntohl(uint32_t netlong);
三、點分十進制IP<===>uint32_t

本節只介紹基于IPv4的socket網路編程,sockaddr_in中的成員struct in_addr sin_addr表示32位 的IP 地址但是我們通常用點分十進制的字串表示IP 地址,以下函式可以在字串表示 和in_addr表示之間轉換;字串轉in_addr的函式,
- 點分十進制IP轉換成為uint32_t
- in_addr_t inet_addr(const char * cp);
- 將字串的點分十進制IP地址轉換為uint32_t
- 將uint32_t從主機位元組序轉換成為網路位元組序,
- uint32_t轉換成為點分十進制IP
- char * inet_ntoa(struct in_addr in);
- 將網路位元組序uint32_t的整數轉換成為主機位元組序,
- 將uint32_t轉換成為點分十進制的字串,
四、UDP的socket編程(流程&介面)
1.UDP的socket編程流程
- cs模型(客戶端服務端):client-server,
- bs模型:瀏覽器-服務器,

1.socket常見API
// 創建 socket 檔案描述符 (TCP/UDP, 客戶端 + 服務器)
int socket(int domain, int type, int protocol);
// 系結埠號 (TCP/UDP, 服務器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
// 開始監聽socket (TCP, 服務器)
int listen(int socket, int backlog);
// 接收請求 (TCP, 服務器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);
2.socketaddr結構的分類
socket API是一層抽象的網路編程介面,適用于各種底層網路協議,如IPv4、 IPv6,以及后面要講的UNIX DomainSocket. 然而, 各種網路協議的地址格式并不相同,

- IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結構體表示,包括16位地址型別, 16位埠號和32位IP地址,
- IPv4、 IPv6地址型別分別定義為常數AF_INET、 AF_INET6. 這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種型別的sockaddr結構體,就可以根據地址型別欄位確定結構體中的內容,
- socket API可以都用struct sockaddr * 型別表示, 在使用的時候需要強制轉化成sockaddr_in; 這樣的好處是程式的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各種型別的sockaddr結構體指標做為引數,
3.socketaddr結構

4.socketaddr_in結構

雖然socket api的介面是sockaddr, 但是我們真正在基于IPv4編程時, 使用的資料結構是sockaddr_in; 這個結構里主要有三部分資訊: 地址型別, 埠號, IP地址,
5.in_addr結構

in_addr用來表示一個IPv4的IP地址. 其實就是一個32位的整數,
2.UDP的socket編程介面
1.創建套接字socket介面
// 創建 socket 檔案描述符 (TCP/UDP, 客戶端 + 服務器)
int socket(int domain, int type, int protocol);

1 #include<iostream>
2 #include<sys/types.h>
3 #include<sys/socket.h>
4 #include<unistd.h>
5
6 using namespace std;
7 int main()
8 {
9 int SockFd=socket(AF_INET,SOCK_STREAM,0);
10 if(SockFd<0)
11 {
12 cout<<"套接字創建失敗!"<<endl;
13 }
14 cout<<"SockFd:"<<SockFd<<endl;
15 while(1)
16 {
17 sleep(1);
18 }
19 return 0;
20 }

2.系結埠號bind介面

// 系結埠號 (TCP/UDP, 服務器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
- sockfd:socket函式回傳的套接字描述符;將創建出來的套接字和網卡,埠好進行系結
- addr:地址資訊結構
- addr的型別是struct sockaddr ,struct sockaddr 是一個通用地址資訊結構,如下圖所示:
假設,定義一個int fun(void * x)引數可以接收任何型別資料的函式,使用時就需要強轉 char* p = “abc”; ? fun((void*)lp);而如上結構體的作用相當于此例中的引數,因為bind函式可能系結 ipv4(uint32_t) / ipv6(uint128_t) / 本地域套接字 等不同型別的協議,所以系結不同版本的IP地址需要提供不同的系結函式,而此做法非常的麻煩,所以將協議的資料結構定義為一個通用的,要使用某一具體的協議,只需傳入具體的協議對應的資料結構并強轉即可,
如下圖所示,我們可以在 vim /usr/include/netinet/in.h 路徑下查看ipv4協議使用的結構體:- addrlen:地址資訊結構的長度(告訴網路協議堆疊最多能決議多少個位元組)
1 #include<iostream>
2 #include<sys/types.h>
3 #include<sys/socket.h>
4 #include<unistd.h>
5 #include<netinet/in.h>
6 #include<arpa/inet.h>
7 using namespace std;
8
9 int main()
10 {
11 int SockFd=socket(AF_INET,SOCK_DGRAM,0);
12 if(SockFd<0)
13 {
14 cout<<"套接字創建失敗!"<<endl;
15 }
16 cout<<"SockFd:"<<SockFd<<endl;
17
18 struct sockaddr_in addr;
19
20 addr.sin_port=htons(20000);
21 addr.sin_family=AF_INET;
22 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
23 int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
24 if(ret<0)
25 {
26 cout<<"系結失敗!"<<endl;
27 return 0;
28 }
29 while(1)
30 {
31 sleep(1);
32 }
33 return 0;
34 }

3.UDP發送介面sendto

ssize_t sendto(int sockfd, const void * buf, size_t len, int flags,const struct sockaddr * dest_addr, socklen_t addrlen);
- sockfd:套接字描述符
- buf:要發送的資料
- len:發送資料的長度
- flags:0 阻塞發送
- dest_addr:目標主機的地址資訊結構(IP,port)
- addrlen:目標主機地址資訊結構的長度
- 回傳值:
成功回傳具體發送的位元組數量,失敗回傳-1
4.UDP接收介面recvform

ssize_t recvfrom(int sockfd, void * buf, size_t len, int flags,struct sockaddr * src_addr, socklen_t * addrlen);
- sockfd:套接字描述符
- buf:將資料接收到buf當中
- len:buf的最大接收能力
- flags:0阻塞接收
- src_addr:這個資料來源的主機的地址資訊結構(IP,port)---->由recvfrom()函式填充
- addrlen:輸入輸出型引數
輸入:在接收之前準備的對端地址資訊結構的長度
輸出:實際接識訓來的地址資訊長度
5.UDP關閉介面close
close(int sockfd);
3.客戶端為什么不推薦系結地址資訊
本質上是不想讓客戶端程式將埠寫死,即不想讓客戶端在啟動的時候,都是系結一個埠的(一個埠只能被一個行程所系結),
eg:客戶端A系結了埠,本機在啟動客戶端B的時候就會系結失敗
??當客戶端沒有主動的系結埠,UDP客戶端在呼叫sendto的時候,會自動系結一個空閑的埠(作業系統分配一個空閑的埠),
五、UDP的socket編程代碼
1.客戶端
客戶端只需創建套接字,向服務端發送請求,接收服務端的回復即可,
1 #include<iostream>
2 #include<stdio.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<unistd.h>
6 #include<netinet/in.h>
7 #include<arpa/inet.h>
8 #include<string.h>
9 #include<stdlib.h>
10 using namespace std;
11
12 int main()
13 {
14 int SockFd=socket(AF_INET,SOCK_DGRAM,0);
15 if(SockFd<0)
16 {
17 cout<<"套接字創建失敗!"<<endl;
18 }
19 cout<<"SockFd:"<<SockFd<<endl;
20
21 /* struct sockaddr_in addr;
22
23 addr.sin_port=htons(20000);
24 addr.sin_family=AF_INET;
25 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
26 int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
27 if(ret<0)
28 {
29 cout<<"系結失敗!"<<endl;
30 return 0;
31 }*/
32 while(1)
33 {
34 char buf[1024]="i am client!";
35 struct sockaddr_in dest_addr;
36 dest_addr.sin_family=AF_INET;
37 dest_addr.sin_port=htons(20000);
38 dest_addr.sin_addr.s_addr=inet_addr("1.14.165.138");
39
40 sendto(SockFd,buf,strlen(buf),0,(struct sockaddr*)&dest_addr,sizeof(dest_addr));
41
42 memset(buf,'\0',sizeof(buf));
43
44 struct sockaddr_in peer_addr;
45 socklen_t len=sizeof(peer_addr);
46
47 ssize_t rece_size=recvfrom(SockFd,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer_addr,&len);
48 if(rece_size<0)
49 {
50 continue;
51 }
52 cout<<"recv msg:"<<buf<<" from "<<inet_ntoa(peer_addr.sin_addr)<<" "<<ntohs(peer_addr.sin_port)<<endl;
53 sleep(1);
54 }
55 close(SockFd);
56 return 0;
57 }
2.服務端
服務端只需創建套接字,系結埠,接收客戶端的請求,回復客戶端資訊即可,
1 #include<iostream>
2 #include<stdio.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<unistd.h>
6 #include<netinet/in.h>
7 #include<arpa/inet.h>
8 #include<string.h>
9 #include<stdlib.h>
10 using namespace std;
11
12 int main()
13 {
14 int SockFd=socket(AF_INET,SOCK_DGRAM,0);
15 if(SockFd<0)
16 {
17 cout<<"套接字創建失敗!"<<endl;
18 }
19 cout<<"SockFd:"<<SockFd<<endl;
20
21 struct sockaddr_in addr;
22
23 addr.sin_port=htons(20000);
24 addr.sin_family=AF_INET;
25 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
26 int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
27 if(ret<0)
28 {
29 cout<<"系結失敗!"<<endl;
30 return 0;
31 }
32 while(1)
33 {
34 char buf[1024]={0};
35 struct sockaddr_in peer_addr;
36 socklen_t len=sizeof(peer_addr);
37 ssize_t rece_size=recvfrom(SockFd,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer_addr,&len);
38 if(rece_size<0)
39 {
40 continue;
41 }
42 cout<<"recv msg:"<<buf<<" from "<<inet_ntoa(peer_addr.sin_addr)<<" "<<ntohs(peer_addr.sin_port)<<endl;
43
44 memset(buf,'\0',sizeof(buf));
45 sprintf(buf,"welcome client %s:%d\n",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port));
46 sendto(SockFd,buf,strlen(buf),0,(struct sockaddr*)&peer_addr,sizeof(peer_addr));
47 }
48 close(SockFd);
49 return 0;
50 }

3.查看埠的使用情況:netstat -anp | grep [埠號]

六、TCP的socket編程(流程&介面)

1.TCP的socket編程流程

2.TCP的socket編程介面
創建套接字介面socket(),系結埠bind(),關閉套接字介面close(),的使用和UDP套接字編程中的使用是一樣的,下面介紹程式中用到的socket API,這些函式都在sys/socket.h中,
1.服務端創建套接字socket介面

- socket()打開一個網路通訊埠,如果成功的話,就像open()一樣回傳一個檔案描述符,
- 應用程式可以像讀寫檔案一樣用read/write在網路上收發資料,
- 如果socket()呼叫出錯則回傳-1,
- 對于IPv4, family引數指定為AF_INET,
- 對于TCP協議,type引數指定為SOCK_STREAM, 表示面向流的傳輸協議,
- protocol引數的介紹從略,指定為0即可,
1 #include<unistd.h>
2 #include<iostream>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/socket.h>
6 #include<arpa/inet.h>
7 #include<error.h>
8 #include<stdio.h>
9 using namespace std;
10
11 int main()
12 {
13 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
14
15 if(listen_sock<0)
16 {
17 perror("socket");
18 return 0;
19 }
20 cout<<listen_sock<<endl;
21 return 0;
22 }

2.服務端系結套接字bind介面

- 服務器程式所監聽的網路地址和埠號通常是固定不變的,客戶端程式得知服務器程式的地址和埠號后就可以向服務器發起連接; 服務器需要呼叫bind系結一個固定的網路地址和埠號,
- bind()成功回傳0,失敗回傳-1,
- bind()的作用是將引數sockfd和myaddr系結在一起, 使sockfd這個用于網路通訊的檔案描述符監聽myaddr所描述的地址和埠號,
1 #include<unistd.h>
2 #include<iostream>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/socket.h>
6 #include<arpa/inet.h>
7 #include<error.h>
8 #include<stdio.h>
9 using namespace std;
10
11 int main()
12 {
13 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
14
15 if(listen_sock<0)
16 {
17 perror("socket");
18 return 0;
19 }
20 cout<<listen_sock<<endl;
21
22 struct sockaddr_in addr;
23
24 addr.sin_family=AF_INET;
25 addr.sin_port=htons(20000);
26 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
27 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");這個地址包含所有本地網卡地址
28 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
29
30 if(ret<0)
31 {
32 perror("bind");
33 return 0;
34 }
35
36 return 0;
37 }


3.服務端監聽套接字listen介面

int listen(int sockfd, int backlog);
- sockfd:套接字描述符
- backlog:已完成連接佇列的大小
- 回傳值:
??成功:0
??失敗:-1
- listen()宣告sockfd處于監聽狀態, 并且最多允許有backlog個客戶端處于連接等待狀態, 如果接收到更多的連接請求就忽略, 這里設定不會太大(一般是5),,
- listen()成功回傳0,失敗回傳-1,
當客戶端和服務端進行三次握手的時候會存在兩種狀態:連接還未建立和連接已建立,此時作業系統內核中就會存在兩個佇列:未完成連接佇列和已完成連接佇列,

如上圖若客戶端只完成①或①②則此連接在未完成連接佇列中,當完成三次握手后會由未完成連接佇列放到已完成連接佇列,而backlog就是已完成連接佇列的大小,backlog影響了服務端并發接收連接的能力,
eg:假設backlog=1,服務端不accepct接收連接,此時有三個客戶端都完成了三次握手,則必有一個客戶端連接進入已完成連接佇列中,由于已完成連接佇列空間不夠,所以剩余兩個客戶端的連接只能放入未完成連接佇列中,
1 #include<unistd.h>
2 #include<iostream>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/socket.h>
6 #include<arpa/inet.h>
7 #include<error.h>
8 #include<stdio.h>
9 using namespace std;
10
11 int main()
12 {
13 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
14
15 if(listen_sock<0)
16 {
17 perror("socket");
18 return 0;
19 }
20 cout<<listen_sock<<endl;
21
22 struct sockaddr_in addr;
23
24 addr.sin_family=AF_INET;
25 addr.sin_port=htons(20000);
26 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
27 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");這個地址包含所有本地網卡地址
28 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
29
30 if(ret<0)
31 {
32 perror("bind");
33 return 0;
34 }
35
36 ret=listen(listen_sock,1);
37 if(ret<0)
38 {
39 perror("listen");
40 return 0;
41 }
42
43 while(1)
44 {
45 sleep(1);
46 }
47
48 return 0;
49 }

4.服務端接收鏈接套接字accept介面

從已經完成連接佇列中獲取已經完成三次握手的連接,沒有連接時,呼叫accept會阻塞,
int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
- sockfd:套接字描述符(listen_sockfd)
- addr:客戶端地址資訊結構(客戶端IP,客戶端的埠)
- addrlen:客戶端地址資訊結構的長度
- 回傳值:
??成功:回傳新連接的套接字描述符
??失敗:回傳-1
三次握手的時候是對listen_sockfd進行操作,當呼叫accept()會在Tcp服務端內部創建一個新的套接字new_sockfd,三次握手之后的資料收發都是多new_sockfd進行操作,如下圖所示:

- 三次握手完成后, 服務器呼叫accept()接受連接,
- 如果服務器呼叫accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來,
- addr是一個傳出引數,accept()回傳時傳出客戶端的地址和埠號,
- addr是一個傳出引數,accept()回傳時傳出客戶端的地址和埠號,
- addrlen引數是一個傳入傳出引數(value-result argument), 傳入的是呼叫者提供的, 緩沖區addr的長度以避免緩沖區溢位問題, 傳出的是客戶端地址結構體的實際長度(有可能沒有占滿呼叫者提供的緩沖區),
5.客戶端連接套接字connect介面

int connect(int sockfd, const struct sockaddr * addr,socklen_t addrlen);
- sockfd:套接字描述符(listen_sockfd)
- addr:服務端地址資訊結構(服務端IP,服務端的埠)
- addrlen:服務端地址資訊結構的長度
- 回傳值:
??成功:回傳0
??小于0,連接失敗
- 客戶端需要呼叫connect()連接服務器,
- connect和bind的引數形式一致, 區別在于bind的引數是自己的地址, 而connect的引數是對方的地址,
6.TCP發送介面send介面

ssize_t send(int sockfd, const void * buf, size_t len, int flags);
- sockfd:套接字描述符(new_sockfd)
- buf:待要發送的資料
- len:發送資料的長度
- flags:
??0:阻塞發送
??MSG_OOB:發送帶外資料
回傳值:
??大于0:回傳發送的位元組數量
??-1:發送失敗
帶外資料:即在緊急情況下所產生的資料,會越過前面進行排隊的數據優先進行發送,
7.TCP接收介面recv介面

ssize_t recv(int sockfd, void * buf, size_t len, int flags);
- sockfd:套接字描述符(new_sockfd)
- buf:將接收的資料放到buf
- len:buf的最大接收能力
- flags:0:阻塞發送;如果客戶端沒有發送資料,呼叫recv會阻塞
- 回傳值:
??大于0:正常接收了多少位元組資料
??等于0:對端將連接關閉了
??小于0:接受失敗
8.TCP關閉介面close介面

3.TCP的連接建立
我們可以用cmd工具telnet模仿TCP三次握手建立連接,在cmd視窗輸入 “tenlet + 公網IP + 埠號” 即可模擬測驗:

按下回車鍵出現以下現象則連接成功:

當我們使用telnet與服務器建立三次連接(即進行三次三次握手)我們會看到,當我們查看服務器埠的使用情況是時會看到如下情況:

雖然我們在代碼中將已完成連接佇列的大小設為1,但上圖已完成連接佇列中卻放了兩個已完成連接,正常情況當我們就backlog設為1,已完成連接佇列中只能放一個已完成連接,那么為什么會出現種情況呢?原因是作業系統內核中判斷已完成佇列是否已滿的邏輯是如下所示:

所以我們設定的bakclog=1,向已完成連接佇列中放入一個已完成連接,queue.size= 1不大于backlog=1,所以再向已完成連接佇列中放入一個已完成連接,此時queue.size= 2大于backlog=1,不再放入,所以就出現如上圖所示現象,雖然我們將backlog設為1,但已完成連接佇列中卻有兩個已完成連接,
4.單行程的TCP的發送和接收資料
1.客戶端代碼
客戶端流程:創建套接字,發起連接connect,發送和接收:
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int sockfd=socket(AF_INET,SOCK_STREAM,0);
15
16 if(sockfd<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<sockfd<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("1.14.165.138");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");這個地址包含所有本地網卡地址
29 int ret=connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("connect");
34 return 0;
35 }
36
37 if(ret<0)
38 {
39 perror("connect");
40 return 0;
41 }
42
43
44 while(1)
45 {
46 char buf[1024]="i am client呀呀呀!";
47
48 send(sockfd,buf,strlen(buf),0);
49 memset(buf,'\0',sizeof(buf));
50 //接收
51 ssize_t recv_size=recv(sockfd,buf,sizeof(buf)-1,0);
52 if(recv_size<0)
53 {
54 perror("recv");
55 return 0;
56 }
57 else if(recv_size==0)
58 {
59 cout<<"close peer connect"<<endl;
60 close(sockfd);
61 continue;
62 }
63
64 cout<<"buf:"<<buf<<endl;
65
66
67
68
69 sleep(1);
70 }
71
72 return 0;
73 }
2.服務端端代碼
服務端流程:創建偵聽套接字,系結地址資訊,監聽,接收新連接accept,接收,發送:
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
15
16 if(listen_sock<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<listen_sock<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");這個地址包含所有本地網卡地址
29 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("bind");
34 return 0;
35 }
36
37 ret=listen(listen_sock,1);
38 if(ret<0)
39 {
40 perror("listen");
41 return 0;
42 }
43
44 struct sockaddr_in cli_addr;
45 socklen_t cli_addrlen=sizeof(cli_addr);
46 int newsockfd=accept(listen_sock,(struct sockaddr*)&cli_addr,&cli_addrlen);
47 if(newsockfd<0)
48 {
49 perror("accept");
50 return 0;
51 }
52
53 cout<<"i accept new connect form client:"<<inet_ntoa(cli_addr.sin_addr)<<" :"<<ntohs(cli_addr.sin_port)<<endl;
54 while(1)
55 {
56 char buf[1024]={0};
57
58 ssize_t recv_size=recv(newsockfd,buf,sizeof(buf)-1,0);
59 if(recv_size<0)
60 {
61 perror("recv");
62 return 0;
63 }
64 else if(recv_size==0)
65 {
66 cout<<"close peer connect"<<endl;
67 close(newsockfd);
68 continue;
69 }
70
71 cout<<"buf:"<<buf<<endl;
72
73 memset(buf,'\0',sizeof(buf));
74 strcpy(buf,"i am serve!!!");
75 send(newsockfd,buf,strlen(buf),0);
76
77
78
79 sleep(1);
80 }
81
82 return 0;
83 }

- 由于客戶端不需要固定的埠號,因此不必呼叫bind(),客戶端的埠號由內核自動分配,
- 客戶端不是不允許呼叫bind(), 只是沒有必要呼叫bind()固定一個埠號. 否則如果在同一臺機器上啟動多個客戶端, 就會出現埠號被占用導致不能正確建立連接,
- 服務器也不是必須呼叫bind(), 但如果服務器不呼叫bind(), 內核會自動給服務器分配監聽埠, 每次啟動服務器時埠號都不一樣, 客戶端要連接服務器就會遇到麻煩,
5.單行程的TCP的發送和接收資料的問題
再啟動一個客戶端, 嘗試連接服務器, 發現第二個客戶端, 不能正確的和服務器進行通信,
分析原因, 是因為我們accecpt了一個請求之后, 就在一直while回圈嘗試read, 沒有繼續呼叫到accecpt, 導致不能接受新的請求


通過pstack查看服務端阻塞在recv處,因為服務端accept接收新連接在while回圈外面,所以服務端在進行一次連接之后會進入while回圈內部,不能再接收新連接(雖然客戶端2和服務端完成了三次握手建立了新連接,但服務端無法接收連接,此時客戶端則無法收到服務端的資料)
TCP單行程存在的問題:當存在多個客戶端與服務器進行通信時,可能會出現recv阻塞或accept阻塞,
若將accept放入while回圈里呢?
將accpect放入while回圈中,則每個客戶端只能收到一條,當客戶端與服務端建立連接,向服務端發送資料服務端,服務端接收資料并回復客戶端,此時服務端將回到while回圈的開始阻塞在accept處(因為之前已經接收客戶端發起的連接,當第二次accept時,已完成連接佇列中就是空佇列),
- 解決辦法:
- 多執行緒,
- 多行程,
6.多執行緒的TCP的發送和接收資料
多行程的客戶端代碼和單行程是一樣的,單行程服務端父行程負責accept,子行程負責資料的接收和發送,需要注意的是,父行程一定要行程等待,防止子行程先于父行程退出使子行程變為僵尸行程,而父行程不能直接父行程的邏輯處使用wait或waitpid進行等待,因為阻塞等待,若子行程一直不退出,則父行程一直在等待,永遠無法接收新連接,我們 需要使用需要使用自定義信號處理方式將SIGCHLD信號重新定義,當子行程退出發出SIGCHLD信號時,父行程則對子行程的資源進行回收,
1.客戶端代碼
創建套接字,發起連接,發送和接收
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int sockfd=socket(AF_INET,SOCK_STREAM,0);
15
16 if(sockfd<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<sockfd<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("1.14.165.138");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");這個地址包含所有本地網卡地址
29 int ret=connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("connect");
34 return 0;
35 }
36
37 if(ret<0)
38 {
39 perror("connect");
40 return 0;
41 }
42
43
44 while(1)
45 {
46 char buf[1024]="i am client!";
47
48 send(sockfd,buf,strlen(buf),0);
49 memset(buf,'\0',sizeof(buf));
50 //接收
51 ssize_t recv_size=recv(sockfd,buf,sizeof(buf)-1,0);
52 if(recv_size<0)
53 {
54 perror("recv");
55 return 0;
56 }
57 else if(recv_size==0)
58 {
59 cout<<"close peer connect"<<endl;
60 close(sockfd);
61 continue;
62 }
63
64 cout<<"buf:"<<buf<<endl;
65
66
67
68
69 sleep(1);
70 }
71
72 return 0;
73 }
2.服務端代碼
服務端主要流程:創建偵聽套接字,系結地址資訊,監聽,接收新連接,創建子行程,接收,發送
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 #include<pthread.h>
11 using namespace std;
12
13 struct ThreadInfo
14 {
15 int _newSockFd;
16 };
17
18 void* TcpThreadStart(void* arg)
19 {
20 pthread_detach(pthread_self());
21 struct ThreadInfo* ti=(struct ThreadInfo*)arg;
22 int newsockfd=ti->_newSockFd;
23
24 while(1)
25 {
26 char buf[1024]={0};
27
28 ssize_t recv_size=recv(newsockfd,buf,sizeof(buf)-1,0);
29 if(recv_size<0)
30 {
31 perror("recv");
32 return 0;
33 }
34 else if(recv_size==0)
35 {
36 cout<<"close peer connect"<<endl;
37 close(newsockfd);
38 continue;
39 }
40
41 cout<<"buf:"<<buf<<endl;
42
43 memset(buf,'\0',sizeof(buf));
44 strcpy(buf,"i am serve!!!");
45 send(newsockfd,buf,strlen(buf),0);
46
47 }
48 delete ti;
49 return NULL;
50 }
51 int main()
52 {
53 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
54
55 if(listen_sock<0)
56 {
57 perror("socket");
58 return 0;
59 }
60 cout<<listen_sock<<endl;
61
62 struct sockaddr_in addr;
63
64 addr.sin_family=AF_INET;
65 addr.sin_port=htons(20000);
66 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
67 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");這個地址包含所有本地網卡地址
68 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
69
70 if(ret<0)
71 {
72 perror("bind");
73 return 0;
74 }
75
76 ret=listen(listen_sock,1);
77 if(ret<0)
78 {
79 perror("listen");
80 return 0;
81 }
82
83 while(1)
84 {
85 struct sockaddr_in cli_addr;
86 socklen_t cli_addrlen=sizeof(cli_addr);
87 int newsockfd=accept(listen_sock,(struct sockaddr*)&cli_addr,&cli_addrlen);
88 if(newsockfd<0)
89 {
90 perror("accept");
91 return 0;
92 }
93
94 cout<<"i accept new connect form client:"<<inet_ntoa(cli_addr.sin_addr)<<" :"<<ntohs(cli_addr.sin_port)<<endl;
95
96 struct ThreadInfo* ti =new ThreadInfo;
97 ti->_newSockFd=newsockfd;
98
99
100 pthread_t tid;
101 ret=pthread_create(&tid,NULL,TcpThreadStart,(void*)ti);
102 if(ret<0)
103 {
104 perror("pthread_create");
105 close(newsockfd);
106 delete ti;
107 continue;
108 }
109
110
111
112
113 sleep(1);
114 }
115
116 return 0;
117 }

7. 多行程的TCP的發送和接收資料
1.客戶端代碼
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int sockfd=socket(AF_INET,SOCK_STREAM,0);
15
16 if(sockfd<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<sockfd<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("1.14.165.138");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");這個地址包含所有本地網卡地址
29 int ret=connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("connect");
34 return 0;
35 }
36
37 if(ret<0)
38 {
39 perror("connect");
40 return 0;
41 }
42
43
44 while(1)
45 {
46 char buf[1024]="i am client!";
47
48 send(sockfd,buf,strlen(buf),0);
49 memset(buf,'\0',sizeof(buf));
50 //接收
51 ssize_t recv_size=recv(sockfd,buf,sizeof(buf)-1,0);
52 if(recv_size<0)
53 {
54 perror("recv");
55 return 0;
56 }
57 else if(recv_size==0)
58 {
59 cout<<"close peer connect"<<endl;
60 close(sockfd);
61 continue;
62 }
63
64 cout<<"buf:"<<buf<<endl;
65
66
67
68
69 sleep(1);
70 }
71
72 return 0;
73 }
2.服務端代碼
1 #include<iostream>
2 #include<errno.h>
3 #include<stdlib.h>
4 #include<sys/socket.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<string.h>
8 #include<arpa/inet.h>
9 #include<netinet/in.h>
10 #include<sys/wait.h>
11 #include<stdio.h>
12 using namespace std;
13
14 void signalcallback(int signo)
15 {
16 //當前的wait是行程等待的阻塞介面, 但是應用的場景一定是子行程退出之后,
17 //父行程收到了SIGCHLD信號之后, 才會回呼sigcallback函式, 才會呼叫wait
18 cout<<"recv signo:"<<signo<<endl;
19 wait(NULL);
20 }
21 int main()
22 {
23 signal(SIGCHLD,signalcallback);
24 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
25 if(listen_sock<0)
26 {
27 perror("socket");
28 return 0;
29 }
30
31 struct sockaddr_in addr;
32
33 addr.sin_family=AF_INET;
34 addr.sin_port=htons(20000);
35 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
36
37 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
38 if(ret<0)
39 {
40 perror("bind");
41 return 0;
42 }
43
44
45 ret=listen(listen_sock,5);
46 if(ret<0)
47 {
48 perror("listen");
49 return 0;
50 }
51
52 while(1)
53 {
54 int new_sock=accept(listen_sock,NULL,NULL); //客服端的地址和埠號,這么傳NULL,不關心客服端的埠號
55 if(new_sock<0)
56 {
57 continue;
58 }
59
60 //創建子行程
61 int pid=fork();
62 if(pid<0)
63 {
64 //創建子行程失敗,但是接收新鏈接成功
65 close(new_sock);
66 continue;
67 }
68 else if(pid==0)
69 {
70 //child
71 close(listen_sock);
72
73 while(1)
74 {
75 //recv and send
76
77 char buf[1024]={0};
78
79 ssize_t recv_size=recv(new_sock,buf,sizeof(buf)-1,0);
80 if(recv_size<0)
81 {
82 perror("recv");
83 continue;
84 }
85 else if(recv_size==0)
86 {
87 cout<<"peer shutdown!"<<endl;
88 close(new_sock);
89
90 //子行程故障
91 //exit(1);
92
93 }
94
95 cout<<"client say:"<<buf<<endl;
96
97 memset(buf,'\0',sizeof(buf));
98 strcpy(buf,"i am serve");
99 send(new_sock,buf,strlen(buf),0);
100 }
101 }
102 else
103 {
104 close(new_sock);
105 }
106 }
107 return 0;
108 }

七、TCP協議通訊流程
- 服務器初始化:
- 呼叫socket, 創建檔案描述符,
- 呼叫bind, 將當前的檔案描述符和ip/port系結在一起; 如果這個埠已經被其他行程占用了, 就會bind失敗,
- 呼叫listen, 宣告當前這個檔案描述符作為一個服務器的檔案描述符, 為后面的accept做好準備,
- 呼叫accecpt, 并阻塞, 等待客戶端連接過來符,
- 建立連接的程序:
- 呼叫socket, 創建檔案描述符,
- 呼叫connect, 向服務器發起連接請求,
- connect會發出SYN段并阻塞等待服務器應答; (第一次),
- 服務器收到客戶端的SYN, 會應答一個SYN-ACK段表示"同意建立連接"; (第二次),
- 客戶端收到SYN-ACK后會從connect()回傳, 同時應答一個ACK段; (第三次),
這個建立連接的程序, 通常稱為 三次握手,
- 資料傳輸的程序:
- 建立連接后,TCP協議提供全雙工的通信服務; 所謂全雙工的意思是, 在同一條連接中, 同一時刻, 通信雙方可以同時寫資料; 相對的概念叫做半雙工, 同一條連接在同一時刻, 只能由一方來寫資料,
- 服務器從accept()回傳后立刻調 用read(), 讀socket就像讀管道一樣, 如果沒有資料到達就阻塞等待,
- 這時客戶端呼叫write()發送請求給服務器, 服務器收到后從read()回傳,對客戶端的請求進行處理, 在此期間客戶端呼叫read()阻塞等待服務器的應答,
- 服務器呼叫write()將處理結果發回給客戶端, 再次呼叫read()阻塞等待下一條請求,
- 客戶端收到后從read()回傳, 發送下一條請求,如此回圈下去,
- 斷開連接的程序:
- 如果客戶端沒有更多的請求了, 就呼叫close()關閉連接, 客戶端會向服務器發送FIN段(第一次),
- 此時服務器收到FIN后, 會回應一個ACK, 同時read會回傳0 (第二次),
- read回傳之后, 服務器就知道客戶端關閉了連接, 也呼叫close關閉連接, 這個時候服務器會向客戶端發送一個FIN; (第三次),
- 客戶端收到FIN, 再回傳一個ACK給服務器; (第四次),
這這個斷開連接的程序, 通常稱為 四次揮手,
- 在學習socket API時要注意應用程式和TCP協議層是如何互動的:
- 應用程式呼叫某個socket函式時TCP協議層完成什么動作,比如呼叫connect()會發出SYN段,
- 用程式如何知道TCP協議層的狀態變化,比如從某個阻塞的socket函式回傳就表明TCP協議收到了某些段,再比如read()回傳0就表明收到了FIN段,
💬總結
以上就是今天要講的內容,本文詳細介紹了網路編程中UDP、和Tcp的編程等知識的使用,網路提供了大量的方法供我們使用,非常的便捷,我們務必掌握,希望大家多多支持!另外如果上述有任何問題,請懂哥指教,不過沒關系,主要是自己能堅持,更希望有一起學習的同學可以幫我指正,但是如果可以請溫柔一點跟我講,愛與和平是永遠的主題,愛各位了,加油啊!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/292036.html
標籤:其他
上一篇:一看就懂的IP協議!!!



