文章目錄:
- 1. TCP的socket程式設計(流程&接口)
-
- 1.1 程式設計流程
- 1.2 TCP的socket程式設計接口
-
- 1.2.1 服務端監聽接口
- 1.2.2 服務端擷取新連接配接
- 1.2.3 用戶端發起連接配接
- 1.2.4 發送接口
- 1.2.4 接收接口
- 2. 判斷端口是否偵聽
- 3. 連接配接建立的現象
- 4. TCP 用戶端 / 服務端(單程序實作)
- 5. TCP單程序版本的問題
- 6. TCP 用戶端 / 服務端(多程序實作)
- 7. TCP 用戶端 / 服務端(多線程實作)
1. TCP的socket程式設計(流程&接口)
1.1 程式設計流程

1.2 TCP的socket程式設計接口
建立套接字接口socket(),綁定端口bind(),關閉套接字接口close(),的使用和UDP套接字程式設計中的使用是一樣的,此處不再提及
1.2.1 服務端監聽接口
int listen(int sockfd, int backlog);
- sockfd:套接字描述符
- backlog:已完成連接配接隊列的大小
傳回值:
成功:0
失敗:-1
當用戶端和服務端進行三次握手的時候會存在兩種狀态:連接配接還未建立和連接配接已建立,此時作業系統核心中就會存在兩個隊列:未完成連接配接隊列和已完成連接配接隊列
如下圖所示:
如上圖若用戶端隻完成①或①②則此連接配接在未完成連接配接隊列中,當完成三次握手後會由未完成連接配接隊列放到已完成連接配接隊列
而backlog就是已完成連接配接隊列的大小,backlog影響了服務端并發接收連接配接的能力
eg:假設backlog=1,服務端不accepct接收連接配接,此時有三個用戶端都完成了三次握手,則必有一個用戶端連接配接進入已完成連接配接隊列中,由于已完成連接配接隊列空間不夠,是以剩餘兩個用戶端的連接配接隻能放入未完成連接配接隊列中
1.2.2 服務端擷取新連接配接
從已經完成連接配接隊列中擷取已經完成三次握手的連接配接,沒有連接配接時,調用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進行操作,如下圖所示:
1.2.3 用戶端發起連接配接
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- sockfd:套接字描述符(listen_sockfd)
- addr:服務端位址資訊結構(服務端IP,服務端的端口)
- addrlen:服務端位址資訊結構的長度
傳回值:
成功:傳回0
小于0,連接配接失敗
1.2.4 發送接口
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd:套接字描述符(new_sockfd)
- buf:待要發送的資料
- len:發送資料的長度
flags:
0:阻塞發送
MSG_OOB:發送帶外資料
傳回值:
大于0:傳回發送的位元組數量
-1:發送失敗
帶外資料:即在緊急情況下所産生的資料,會越過前面進行排隊的資料優先進行發送
1.2.4 接收接口
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:接受失敗
2. 判斷端口是否偵聽
寫一個Tcp服務端代碼,讓其建立套接字,然後綁定位址資訊,然後監聽,最後寫一個while死循環友善我們看現象,代碼如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/socket.h>
4 #include<arpa/inet.h>
5
6 int main()
7 {
8 int listen_sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
9 if(listen_sockfd < 0)
10 {
11 perror("socket");
12 return 0;
13 }
14
15 struct sockaddr_in addr;
16 addr.sin_family = AF_INET;
17 addr.sin_port = htons(18989);
18 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
19 int ret = bind(listen_sockfd,(struct sockaddr*)& addr,sizeof(addr));
20 if(ret < 0)
21 {
22 perror("bind");
23 return 0;
24 }
25
26 ret = listen(listen_sockfd,1);
27 if(ret < 0)
28 {
29 perror("listen");
30 return 0;
31 }
32
33 while(1)
34 {
35 sleep(1);
36 }
37 return 0;
38 }
讓如上代碼跑起來,我們應該看到程式在sleep,端口狀态為監聽狀态,如下圖所示:
3. 連接配接建立的現象
我們可以用windows下的工具telnet模仿TCP三向交握建立連接配接,在windows下的cmd視窗輸入 “tenlet + 公網IP + 端口号” 即可模拟測試
測試如下:
首先我們讓2中的服務端程式跑起來,然後在cmd中使用telnet進行測試
回車後若如下圖所示,則建立連接配接成功
當我們使用telnet與伺服器建立三次連接配接(即進行三次三次握手)我們會看到,當我們檢視伺服器端口的使用情況是時會看到如下情況:
雖然我們在代碼中将已完成連接配接隊列的大小設為1,但上圖已完成連接配接隊列中卻放了兩個已完成連接配接,正常情況當我們就backlog設為1,已完成連接配接隊列中隻能放一個已完成連接配接,那麼為什麼會出現種情況呢?原因是作業系統核心中判斷已完成隊列是否已滿的邏輯是如下所示:
if(queue.size > backlog)
{
//不再往已完成連接配接隊列中放
//不再建立連接配接
}
是以我們設定的bakclog=1,向已完成連接配接隊列中放入一個已完成連接配接,queue.size= 1不大于backlog=1,是以再向已完成連接配接隊列中放入一個已完成連接配接,此時queue.size= 2大于backlog=1,不再放入,是以就出現如上圖所示現象,雖然我們将backlog設為1,但已完成連接配接隊列中卻有兩個已完成連接配接。
4. TCP 用戶端 / 服務端(單程序實作)
用戶端主要流程:建立套接字,發起連接配接,發送和接收
用戶端代碼如下:
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6
7 int main()
8 {
9 int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
10 if(sockfd < 0)
11 {
12 perror("socket");
13 return 0;
14 }
15
16 struct sockaddr_in addr;
17 addr.sin_family = AF_INET;
18 addr.sin_port = htons(18989);
19 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
20 int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
21 if(ret < 0)
22 {
23 perror("connect");
24 return 0;
25 }
26
27
28 while(1)
29 {
30
31 sleep(1);
32
33 char buf[1024] = {0};
34 sprintf(buf,"hello server,i am client\n");
35 ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
36 if(send_ret < 0)
37 {
38 perror("send");
39 continue;
40 }
41
42
43 memset(buf,'\0',sizeof(buf));
44 ssize_t recv_ret = recv(sockfd,buf,sizeof(buf)-1,0);
45 if(recv_ret < 0)
46 {
47 perror("recv");
48 continue;
49 }
50 else if(recv_ret == 0)
51 {
52 printf("server close");
53
54 close(sockfd);
55
56 return 0;
57 }
58
59 printf("server say: %s\n",buf);
60 }
61
62 close(sockfd);
63 return 0;
64 }
服務端主要流程:建立偵聽套接字,綁定位址資訊,監聽,接收新連接配接,接收,發送
服務端代碼如下:
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6
7 int main()
8 {
9 int listen_sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
10 if(listen_sockfd < 0)
11 {
12 perror("socket");
13 return 0; 14 }
15
16 struct sockaddr_in addr;
17 addr.sin_family = AF_INET;
18 addr.sin_port = htons(18989);
19 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
20 int ret = bind(listen_sockfd,(struct sockaddr*)&addr,sizeof(addr));
21 if(ret < 0)
22 {
23 perror("bind");
24 return 0;
25 }
26
27 ret = listen(listen_sockfd,1);
28 if(ret < 0)
29 {
30 perror("listen");
31 return 0;
32 }
33
34 struct sockaddr_in peer_addr;
35 socklen_t socklen = sizeof(peer_addr);
36 int new_sockfd = accept(listen_sockfd,(struct sockaddr*)&peer_addr,&socklen);
37 if(new_sockfd < 0)
38 {
39 perror("accept");
40 return 0;
41 }
42
43 while(1)
44 {
45 char buf[1024] = {0};
46 ssize_t recv_ret = recv(new_sockfd,buf,sizeof(buf)-1,0);
47 if(recv_ret < 0)
48 {
49 perror("recv");
50 continue;
51 }
52 else if(recv_ret == 0)
53 {
54 printf("client close");
55 close(new_sockfd);
56 close(listen_sockfd);
57 return 0;
58 }
59
60 printf("client%d say:%s%d",new_sockfd,buf,new_sockfd);
61
62 memset(buf,'\0',sizeof(buf));
63 sprintf(buf,"hello,i am server,i recv client ip is %s,port is %d",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port));
64 ssize_t send_ret = send(new_sockfd,buf,strlen(buf),0);
65 if(send_ret < 0)
66 {
67 perror("send");
68 continue;
69 }
70
71 }
72 close(new_sockfd);
73 close(listen_sockfd);
74 return 0;
75 }
運作結果如下:
用戶端
服務端
5. TCP單程序版本的問題
如上4中所示,TCP單程序版本運作結果一切都符合預期,但如果再來一個用戶端和服務端進行通信會是什麼結果呢?
如下圖所示:
如上圖所示,我們可以看到第二個用戶端雖然跑起來了,但沒有輸出來自服務端發送的資料,這是為什麼呢?
通過pstack檢視用戶端阻塞在recv處,因為服務端accept接收新連接配接在while循環外面,是以服務端在進行一次連接配接之後會進入while循環内部,不能再接收新連接配接(雖然用戶端2和服務端完成了三次握手建立了新連接配接,但服務端無法接收連接配接,此時用戶端則無法收到服務端的資料)
若将accept放入while循環裡呢?
将accpect放入while循環中,則每個用戶端隻能收到一條,當用戶端與服務端建立連接配接,向服務端發送資料服務端,服務端接收資料并回複用戶端,此時服務端将回到while循環的開始阻塞在accept處(因為之前已經接收用戶端發起的連接配接,當第二次accept時,已完成連接配接隊列中就是空隊列)
TCP單程序存在的問題:當存在多個用戶端與伺服器進行通信時,可能會出現recv阻塞或accept阻塞
解決辦法:
①使用多程序
②使用多線程
③使用多路轉接的技術
6. TCP 用戶端 / 服務端(多程序實作)
多程序的用戶端代碼和單程序是一樣的,單程序服務端父程序負責accept,子程序負責資料的接收和發送,需要注意的是,父程序一定要程序等待,防止子程序先于父程序退出使子程序變為僵屍程序,而父程序不能直接父程序的邏輯處使用wait或waitpid進行等待,因為阻塞等待,若子程序一直不退出,則父程序一直在等待,永遠無法接收新連接配接,我們 需要使用需要使用自定義信号處理方式将SIGCHLD信号重新定義,當子程序退出發出SIGCHLD信号時,父程序則對子程序的資源進行回收
用戶端主要流程:建立套接字,發起連接配接,發送和接收
用戶端代碼如下:
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6
7 int main()
8 {
9 int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
10 if(sockfd < 0)
11 {
12 perror("socket");
13 return 0;
14 }
15
16 struct sockaddr_in addr;
17 addr.sin_family = AF_INET;
18 addr.sin_port = htons(18989);
19 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
20 int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
21 if(ret < 0)
22 {
23 perror("connect");
24 return 0;
25 }
26
27
28 while(1)
29 {
30
31 sleep(1);
32
33 char buf[1024] = {0};
34 sprintf(buf,"hello server,i am client1");
35 ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
36 if(send_ret < 0)
37 {
38 perror("send");
39 continue;
40 }
41
42
43 memset(buf,'\0',sizeof(buf));
44 ssize_t recv_ret = recv(sockfd,buf,sizeof(buf)-1,0);
45 if(recv_ret < 0)
46 {
47 perror("recv");
48 continue;
49 }
50 else if(recv_ret == 0)
51 {
52 printf("server close");
53
54 close(sockfd);
55
56 return 0;
57 }
58
59 printf("server say: %s\n",buf);
60 }
61
62 close(sockfd);
63 return 0;
64 }
服務端主要流程:建立偵聽套接字,綁定位址資訊,監聽,接收新連接配接,建立子程序,接收,發送
服務端代碼如下:
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6 #include<signal.h>
7 #include<sys/wait.h>
8
9
10 void sigcallback(int signo)
11 {
12 wait(NULL);
13 }
14
15 int main()
16 {
18 signal(SIGCHLD,sigcallback);
19
20
21 int listen_sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
22 if(listen_sockfd < 0)
23 {
24 perror("socket");
25 return 0;
26 }
27
28 struct sockaddr_in addr;
29 addr.sin_family = AF_INET;
30 addr.sin_port = htons(18989);
31 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
32 int ret = bind(listen_sockfd,(struct sockaddr*)&addr,sizeof(addr));
33 if(ret < 0)
34 {
35 perror("bind");
36 return 0;
37 }
38
39 ret = listen(listen_sockfd,1);
40 if(ret < 0)
41 {
42 perror("listen");
43 return 0;
44 }
45
46
47 while(1)
48 {
49 //接收連接配接
50 struct sockaddr_in peer_addr;
51 socklen_t socklen = sizeof(peer_addr);
52 int new_sockfd = accept(listen_sockfd,(struct sockaddr*)&peer_addr,&socklen);
53 if(new_sockfd < 0)
54 {
55 perror("accept");
56 return 0;
57 }
58
59 pid_t f_ret = fork();
60 if(f_ret < 0)
61 {
62 perror("fork");
63 continue;
64 }
65 else if(f_ret == 0)
66 {
67 close(listen_sockfd);
68 //child
69
70 while(1)
71 {
72 //接收
73 char buf[1024] = {0};
74 ssize_t recv_ret = recv(new_sockfd,buf,sizeof(buf)-1,0);
75 if(recv_ret < 0)
76 {
77 perror("recv");
78 continue;
79 }
80 else if(recv_ret == 0)
81 {
82 printf("client close");
83 close(new_sockfd);
84 return 0;
85 }
86
87 printf("client new_sockfd:%d say:%s\n",new_sockfd,buf);
88
89
90 //發送
91 memset(buf,'\0',sizeof(buf));
92 sprintf(buf,"hello,i am server,i recv client%d ip is %s,port is %d",new_sockfd,inet _ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port));
93 ssize_t send_ret = send(new_sockfd,buf,strlen(buf),0);
94 if(send_ret < 0)
95 {
96 perror("send");
97 continue;
98 }
99
100 }
101 }
102 else
103 {
104 //father
105 //防止用戶端關閉,子程序直接return 0退出産生僵屍程序
106 //是以父程序需要進行等待,但不能在此次使用wait或waitpid
107 //因為阻塞等待,若子程序一直不退出,則父程序一直在等待,永遠無法接收新連接配接
108 //需要使用自定義信号處理方式
109 close(new_sockfd);
110 continue;
111 }
112 }
113 return 0;
114 }
運作結果如下:
用戶端1:
用戶端2:
服務端:
7. TCP 用戶端 / 服務端(多線程實作)
Tcp_thread_server.hpp 服務端接口封裝(聲明定義可以放一起):
1 #pragma once
2 #include<stdio.h>
3 #include<string>
4 #include<string.h>
5 #include<unistd.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<signal.h>
9 #include<sys/wait.h>
10 #include<pthread.h>
11
12 using namespace std;
13
14 class TcpSocket
15 {
16 public:
17
18 TcpSocket():sockfd_(-1)
19 {}
20 ~TcpSocket()
21 {}
22
23 void SetSockfd(int sockfd)
24 {
25 sockfd_ = sockfd;
26 }
27 //建立套接字
28 int Socket()
29 {
30 sockfd_ = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
31 if(sockfd_ < 0)
32 {
33 perror("socket");
34 return -1;
35 }
36 return sockfd_;
37 }
38
39 //綁定位址資訊
40 int Bind(const string& ip = "0.0.0.0",uint16_t port = 18989)
41 {
42 struct sockaddr_in addr;
43 addr.sin_family = AF_INET;
44 addr.sin_port = htons(port);
45 addr.sin_addr.s_addr = inet_addr(ip.c_str());
46 int ret = bind(sockfd_,(struct sockaddr*)&addr,sizeof(addr));
47 if(ret < 0)
48 {
49 perror("bind");
50 return -1;
51 }
52 return ret;
53 }
54
55 //偵聽
56 int Listen(int backlog = 5)
57 {
58 int ret = listen(sockfd_,backlog);
59 if(ret < 0)
60 {
61 perror("listen");
62 return -1;
63 }
64 return ret;
65 }
66
67 //接收新連接配接
68 int Accept(struct sockaddr_in* addr)
69 {
70 socklen_t socklen = sizeof(struct sockaddr_in);
71 int new_sockfd = accept(sockfd_,(struct sockaddr*)addr,&socklen);
72 if(new_sockfd < 0)
73 {
74 perror("accept");
75 addr = NULL;
76 return -1;
77 }
78 return new_sockfd;
79 }
80
81 //接收
82 ssize_t Recv(string* data)
83 {
84 data->clear();
85 char buf[1024] = {0};
86 ssize_t recv_ret = recv(sockfd_,buf,sizeof(buf)-1,0);
87 if(recv_ret < 0)
88 {
89 perror("recv");
90 return -1;
91 }
92 else if(recv_ret == 0)
93 {
94 printf("client close");
95
96 return -2;
97 }
98 data->assign(buf,strlen(buf));
99 return recv_ret;
100 }
101
102 //發送
103 ssize_t Send(const string& data)
104 {
105 size_t send_ret = send(sockfd_,data.c_str(),data.size(),0);
106 if(send_ret < 0)
107 {
108 perror("send");
109 return -1;
110 }
111 return send_ret;
112 }
113
114 //關閉套接字
115 void Close()
116 {
117 close(sockfd_);
118
119 sockfd_ = -1;
120 }
121
122 private:
123 int sockfd_;
124 };
用戶端:
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6
7 int main()
8 {
9 int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
10 if(sockfd < 0)
11 {
12 perror("socket");
13 return 0;
14 }
15
16 struct sockaddr_in addr;
17 addr.sin_family = AF_INET;
18 addr.sin_port = htons(18989);
19 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
20 int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
21 if(ret < 0)
22 {
23 perror("connect");
24 return 0;
25 }
26
27
28 while(1)
29 {
30
31 sleep(1);
32
33 char buf[1024] = {0};
34 sprintf(buf,"hello server,i am client2");
35 ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
36 if(send_ret < 0)
37 {
38 perror("send");
39 continue;
40 }
41
42
43 memset(buf,'\0',sizeof(buf));
44 ssize_t recv_ret = recv(sockfd,buf,sizeof(buf)-1,0);
45 if(recv_ret < 0)
46 {
47 perror("recv");
48 continue;
49 }
50 else if(recv_ret == 0)
51 {
52 printf("server close");
53
54 close(sockfd);
55
56 return 0;
57 }
58
59 printf("server say: %s\n",buf);
60 }
61
62 close(sockfd);
63 return 0;
64 }
服務端:
1 #include"Tcp_thread_server.hpp"
2 #include<pthread.h>
3
4 void* MyThreadStart(void* arg)
5 {
6 TcpSocket* ts = (TcpSocket*)arg;
7
8 while(1)
9 {
10 string data = "";
11 int recv_ret = ts->Recv(&data);
12 if(recv_ret < 0)
13 {
14 printf("recv fail");
15 continue;
16 }
17 else if(recv_ret == -2)
18 {
19 printf("client close\n");
20 ts->Close();
21 delete ts;
22 return 0;
23 }
24 printf("client say:%s\n",data.c_str());
25
26 data.clear();
27 data.assign("hello client,i am server");
28 ssize_t send_ret = ts->Send(data);
29 if(send_ret < 0)
30 {
31 printf("send fail\n");
32 continue;
33 }
34 }
35 return NULL;
36 }
37
38 int main()
39 {
40 TcpSocket tcp;
41
42 int ret = tcp.Socket();
43 if(ret < 0)
44 {
45 return -1;
46 }
47
48 ret = tcp.Bind();
49 if(ret < 0)
50 {
51 return -1;
52 }
53
54 ret = tcp.Listen();
55 if(ret < 0)
56 {
57 return -1;
58 }
59
60 while(1)
61 {
62 struct sockaddr_in addr;
63 int new_sock = tcp.Accept(&addr);
64 if(new_sock < 0)
65 {
66 continue;
67 }
68
69 TcpSocket* ts = new TcpSocket();
70 if(ts == NULL)
71 {
72 close(new_sock);
73 continue;
74 }
75
76 ts->SetSockfd(new_sock);
77
78 pthread_t tid;
79 int ret = pthread_create(&tid,NULL,MyThreadStart,(void*)ts);
80 if(ret < 0)
81 {
82 perror("pthread_create");
83
84 ts->Close();
85 delete ts;
86 continue;
87 }
88
89 //線程分離
90 pthread_detach(tid);
91 }
92 return 0;
93 }
運作結果如下:
用戶端:
服務端: