一、简单介绍TCP
面向连接:TCP的客户端与服务端进行通信时,服务端需要先和客户端建立连接,确认双方都在线后,再进行发送数据 可靠:保证数据是可靠有序到达对端的 面向字节流:多次发送的数据在网络传输过程中是没有明显边界的(TCP粘包问题)。如客户端发送先发送123,再发送abc,TCP能保证接收到的数据是123abc有序的。但不能确定123和abc之间是分多少次发送的。
二、TCP编程流程
1.编程流程
TCP与UDP的区别在于,TCP是可靠传输的,在连接之前需要双方建立连接。 对于服务端而言,前期的准备工作是:1.创建套接字 2.绑定地址信息 3.监听 对于客户端而言,准备工作是:1.创建套接字 2.发起连接
2.TCP的发送/接收缓冲区
对于TCP发送/接收缓冲区的理解对了解整个TCP编程流程十分重要。 当一个新连接来到的时候,监听接口中的侦听套接字知道有一个连接请求(接收到一个新连接),但不是这个侦听套接字与客户端进行通信的,是侦听套接字调用accept函数创建一个新连接套接字,这个新连接套接字对客户端进行服务。每有一个新连接到来侦听套接字就会创建一个新连接套接字。侦听套接字只负责接收新连接。 然后收发数据流程就变成了客户端将数据放到客户端的发送缓冲区,经过网络协议栈层层封装到达对端(服务端),经过网络协议栈的层层分用达到服务端的接收缓冲区(新连接套接字的接收缓冲区),服务端再从接收缓冲区中拿到数据,从而实现通信。
三、编程接口
创建套接字和绑定地址信息的接口都和UDP协议中的接口相同,都是使用socket函数与bind函数,下面是TCP编程独有的接口
1.监听(listen)
在linten函数中,对参数backlog的理解十分重要,它表示内核当中已完成连接队列的大小。 操作系统内核当中有一个未完成连接队列和一个已完成连接队列。在调用listen函数进行监听等待连接时,当客户端发起连接,就将这个连接放入到未完成连接队列当中。客户端服务端一旦进行三次握手建立连接后,这个连接就会从未完成连接队列中放入到已完成连接队列中,服务端调用accept函数是从这个已完成连接队列中拿数据,将这个连接拿走并创建新连接套接字来服务。 已完成连接队列大小backlog决定了服务端的并发连接数(指的是同一时刻服务端能处理的最大连接数量上限,不是服务端能够接收的连接上限) 服务端能够接收的连接上限取决于操作系统对进程中打开文件描述符的上限。因为每有一个新的连接就是一个套接字描述符,套接字描述符本质也是文件描述符。 能和服务端建立连接的数量 = backlog + 1
2.客户端连接(connect)
3.服务端接收连接(accept)
accept函数是一个阻塞函数
4.发送数据(send)
5.接收数据(recv)
四、TCPsocket编程
对于UDP编程并没有引入多线程编程与多进程编程的概念,都是单线程编程,是因为UDP之间的收发数据较为简单,双方的收发数据都是由一个套接字来完成的。 TCP也存在单线程编程,但牵扯到accept创建的多个新连接套接字时,单线程编程就会出现一些问题:在编程中需要使用while循环进行循环接收连接,但使用while循环时, 1.当accept函数放到while循环中,tcp服务端只能接收一个客户端的新连接(不代表只能有一个客户端建立连接),并和这一个客户端进行多次收发数据。 2.当accept函数放到while循环外,可以接收多个客户端新连接,但每个连接只能收发一次。 为了解决单线程编程的问题,引入了多线程编程和多进程编程来解决问题。 客户端代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <string.h>
4 #include <sys/socket.h>
5 #include <netinet/in.h>
6 #include <arpa/inet.h>
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(27015);
19 addr.sin_addr.s_addr = inet_addr("1.14.151.67");
20
21 int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
22 if(ret < 0)
23 {
24 perror("connect");
25 return 0;
26 }
27
28 while(1)
29 {
30 char buf[1024] = "我是客户端!";
31
32 send(sockfd, buf, strlen(buf), 0);
33 memset(buf, '\0', sizeof(buf));
34
35 ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
36 if(recv_size < 0)
37 {
38 perror("recv");
39 continue;
40 }
41 else if(recv_size == 0)
42 {
43 printf("对端关闭连接\n");
44 close(sockfd);
45 return 0;
46 }
47 printf("%s\n", buf);
48 sleep(1);
49 }
50 close(sockfd);
51 return 0;
52 }
1.多线程编程
多线程编程流程:为了解决单线程的弊端,采用多线程的方式,让主线程去监听(只接收数据,不处理数据),然后主线程创建一堆工作线程,让工作线程去与客户端进行通信。 注意:多线程编程时需要给当前线程设置分离属性,让操作系统回收线程退出的资源,使用new在堆上开辟的空间需要delete释放 多线程服务端代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <string.h>
4 #include <pthread.h>
5 #include <sys/socket.h>
6 #include <netinet/in.h>
7 #include <arpa/inet.h>
8
9 struct ThreadInfo
10 {
11 int newsockfd_;
12 };
13
14 void* TcpThreadStart(void* arg)
15 {
16 pthread_detach(pthread_self());
17 struct ThreadInfo* ti = (struct ThreadInfo*)arg;
18 int newsockfd = ti->newsockfd_;
19 while(1)
20 {
21
22 char buf[1024] = {0};
23 ssize_t recv_size = recv(newsockfd, buf, sizeof(buf) - 1, 0);
24 if(recv_size < 0)
25 {
26 perror("recv");
27 continue;
28 }
29 else if(recv_size == 0)
30 {
31 printf("对端关闭连接\n");
32 close(newsockfd);
33 break;
34 }
35 printf("%s\n", buf);
36 memset(buf, '\0', sizeof(buf));
37 strcpy(buf, "我是服务端!!!");
38
39 send(newsockfd, buf, strlen(buf), 0);
40 }
41 delete ti;
42 return NULL;
43 }
44
45 int main()
46 {
47 int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
48 if(listen_sock < 0)
49 {
50 perror("socket");
51 return 0;
52 }
53
54 struct sockaddr_in addr;
55 addr.sin_family = AF_INET;
56 addr.sin_port = htons(27015);
57
58 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
59
60 int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
61 if(ret < 0)
62 {
63 perror("bind");
64 return 0;
65 }
66 ret = listen(listen_sock, 1);
67 if(ret < 0)
68 {
69 perror("listen");
70 return 0;
71 }
72 while(1)
73 {
74 struct sockaddr_in cli_addr;
75 socklen_t cli_addrlen = sizeof(cli_addr);
76
77 int newsockfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_addrlen);
78 if(newsockfd < 0)
79 {
80 perror("accept");
81 return 0;
82 }
83 printf("从客户端 %s:接收到新连接,端口:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
84 struct ThreadInfo* ti = new ThreadInfo;
85 ti->newsockfd_ = newsockfd;
86
87 pthread_t tid;
88 ret = pthread_create(&tid, NULL, TcpThreadStart, (void*)ti);
89 if(ret < 0)
90 {
91 close(newsockfd);
92 delete ti;
93 continue;
94 }
95 }
96 close(listen_sock);
97 return 0;
98 }
2.多进程编程
多进程编程流程:与多线程编程原理类似,让父进程进行监听(只接收数据,不处理数据),然后父进程创建子进程,让子进程与客户端进行通信。 注意:在创建子进程后,需要使用自定义信号处理方式修改父进程对于SIGCHLD信号的处理方式,不然子进程先退出之后就会产生僵尸进程,造成内存泄漏。 服务端代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include <signal.h>
6 #include <sys/wait.h>
7 #include <sys/socket.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10
11 void sigcallback(int signo)
12 {
13 printf("收到信号 : %d\n", signo);
14
15
16 wait(NULL);
17 }
18
19 int main()
20 {
21
22 signal(SIGCHLD, sigcallback);
23
24 int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
25 if(listen_sock < 0)
26 {
27 perror("socket");
28 return 0;
29 }
30
31 struct sockaddr_in addr;
32 addr.sin_family = AF_INET;
33 addr.sin_port = htons(27015);
34 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
35 int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
36 if(ret < 0)
37 {
38 perror("bind");
39 return 0;
40 }
41
42 ret = listen(listen_sock, 5);
43 if(ret < 0)
44 {
45 perror("listen");
46 return 0;
47 }
48
49
50 while(1)
51 {
52 int new_sock = accept(listen_sock, NULL, NULL);
53 if(new_sock < 0)
54 {
55 continue;
56 }
57
58
59 int pid = fork();
60 if(pid < 0)
61 {
62
63 close(new_sock);
64 continue;
65 }
66 else if(pid == 0)
67 {
68
69 close(listen_sock);
70 while(1)
71 {
72
73 char buf[1024] = {0};
74
75 ssize_t recv_size = recv(new_sock, buf, sizeof(buf) - 1, 0);
76 if(recv_size < 0)
77 {
78 perror("recv");
79 continue;
80 }
81 else if(recv_size == 0)
82 {
83 printf("对端关闭连接\n");
84 close(new_sock);
85
86 exit(1);
87 }
88 printf("client say: \"%s\"\n", buf);
89 memset(buf, '\0', sizeof(buf));
90 strcpy(buf, "我是服务端!");
91 send(new_sock, buf, strlen(buf), 0);
92 }
93 }
94 else
95 {
96
97 close(new_sock);
98 }
99 }
100 return 0;
101 }
总结
TCP的socket编程相对于UDPsocket编程更为复杂,但各有优劣。学习TCP及UDP的网络编程有助于理解传输层协议TCP和UDP。
|