创建套接字接口socket(),绑定端口bind(),关闭套接字接口close(),的使用和UDP套接字编程中的使用是一样的,此处不再提及
int listen(int sockfd, int backlog);
- sockfd:套接字描述符
- backlog:已完成连接队列的大小
- 返回值:
成功:0
失败:-1
当客户端和服务端进行三次握手的时候会存在两种状态:连接还未建立和连接已建立,此时操作系统内核中就会存在两个队列:未完成连接队列和已完成连接队列
如上图若客户端只完成①或①②则此连接在未完成连接队列中,当完成三次握手后会由未完成连接队列放到已完成连接队列
而backlog就是已完成连接队列的大小,backlog影响了服务端并发接收连接的能力
eg:假设backlog=1,服务端不accepct接收连接,此时有三个客户端都完成了三次握手,则必有一个客户端连接进入已完成连接队列中,由于已完成连接队列空间不够,所以剩余两个客户端的连接只能放入未完成连接队列中
从已经完成连接队列中获取已经完成三次握手的连接,没有连接时,调用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进行操作,如下图所示:
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- sockfd:套接字描述符(listen_sockfd)
- addr:服务端地址信息结构(服务端IP,服务端的端口)
- addrlen:服务端地址信息结构的长度
- 返回值:
成功:返回0
小于0,连接失败
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd:套接字描述符(new_sockfd)
- buf:待要发送的数据
- len:发送数据的长度
- flags:
0:阻塞发送
MSG_OOB:发送带外数据- 返回值:
大于0:返回发送的字节数量
-1:发送失败
带外数据:即在紧急情况下所产生的数据,会越过前面进行排队的数据优先进行发送
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:接受失败
写一个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,端口状态为监听状态,如下图所示:
我们可以用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,但已完成连接队列中却有两个已完成连接。
客户端主要流程:创建套接字,发起连接,发送和接收
客户端代码如下:
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 }
运行结果如下:
如上4中所示,TCP单进程版本运行结果一切都符合预期,但如果再来一个客户端和服务端进行通信会是什么结果呢?
如下图所示:
如上图所示,我们可以看到第二个客户端虽然跑起来了,但没有输出来自服务端发送的数据,这是为什么呢?
通过pstack查看客户端阻塞在recv处,因为服务端accept接收新连接在while循环外面,所以服务端在进行一次连接之后会进入while循环内部,不能再接收新连接(虽然客户端2和服务端完成了三次握手建立了新连接,但服务端无法接收连接,此时客户端则无法收到服务端的数据)
若将accept放入while循环里呢?
将accpect放入while循环中,则每个客户端只能收到一条,当客户端与服务端建立连接,向服务端发送数据服务端,服务端接收数据并回复客户端,此时服务端将回到while循环的开始阻塞在accept处(因为之前已经接收客户端发起的连接,当第二次accept时,已完成连接队列中就是空队列)
TCP单进程存在的问题:当存在多个客户端与服务器进行通信时,可能会出现recv阻塞或accept阻塞
解决办法:
①使用多进程
②使用多线程
③使用多路转接的技术
多进程的客户端代码和单进程是一样的,单进程服务端父进程负责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 }
运行结果如下:
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 }
运行结果如下: