TCP网络通讯
TCP编程流程
接口介绍
-
socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
-
**bind()方法是用来指定套接字使用的 IP 地址和端口。**IP 地址就是自己主机的地址,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值,一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在Linux 上,1024 以内的端口号,只有 root 用户可以使用。
-
**listen()方法是用来创建监听队列。**监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
-
accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
-
connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接。
-
send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。
-
recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞;返回值是实际读到的字节数,如果recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
-
close()方法用来关闭 TCP 连接。此时,会进行四次挥手。
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);assert(sockfd != -1);struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (-1 == res){ exit(1);}while (1){char buff[128] = {0};printf("input:\n");fgets(buff, 128, stdin);if (strncmp(buff, "end", 3) == 0){break;}send(sockfd, buff, strlen(buff), 0);memset(buff, 0, 128);recv(sockfd, buff, 127, 0);printf("buff=%s\n", buff);}close(sockfd);exit(0);
}
服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(-1 == sockfd){exit(1);}struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000); // htons 将主机字节序转换为网络字节saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (-1 == res){exit(1);}res = listen(sockfd, 5);if (-1 == res){exit(1);}struct sockaddr_in caddr;socklen_t len = sizeof(caddr);int n = 0;int c = -1;while (1) // 服务器循环接收客户端连接{char data[128] = {0};if (n == 0){c = accept(sockfd, (struct sockaddr *)&caddr, &len); // 阻塞if (c == -1){printf("accept error ");continue;;}}n = recv(c, data, 127, 0); // 阻塞if (n == 0) //连接关闭{close(c);printf("client close\n");continue;}else if (n < 0) //出错{printf("recv error");continue;}printf("n = %d, buff = %s\n", n, data);send(c, "OK", 2, 0);}close(sockfd); exit(0);
}
运行结果:
引入多线程处理并发
服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>void *run(void *arg)
{int c = (int)arg;while (1){char buff[128] = {0};if (recv(c, buff, 127, 0) <= 0){break;}printf("recv(%d)=%s", c, buff);send(c, "ok", 2, 0);}printf("one client over(%d)\n", c);close(c);
}int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == sockfd){exit(1);}struct sockaddr_in saddr, caddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (-1 == res){exit(1);}listen(sockfd, 10);while (1){int len = sizeof(caddr);int c = accept(sockfd, (struct sockaddr *)&caddr, &len);if (c < 0){continue;}printf("accept c = %d\n", c);pthread_t id;pthread_create(&id, NULL, run, (void *)c);}close(sockfd);exit(0);
}
运行结果:
引入fork处理并发
服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>void DealClientLink(int c, struct sockaddr_in caddr)
{while (1){char buff[128] = {0};int n = recv(c, buff, 127, 0);if (n <= 0){break;}printf("%s:%d %s", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port), buff);send(c, "OK", 2, 0);}printf("One Client Close\n");close(c);
}void Signal_Fun(int sign)
{wait(NULL);
}
int main()
{signal(SIGCHLD, Signal_Fun); // 用wait()处理僵死进程int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == sockfd){printf("create sockfd error\n");exit(1);}struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));assert(-1 != res);listen(sockfd, 10);while (1){struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd, (struct sockaddr *)&caddr, &len);assert(-1 != c);printf("%s:%d Link Success\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));pid_t pid = fork();if (-1 == pid){exit(1);}if (0 == pid){DealClientLink(c,caddr);exit(0); //必须结束子进程,否则会有多个进程调 accept}else{close(c); //父子进程都需要关闭 c}}close(sockfd);exit(0);
}
运行结果:
TCP连接状态转变图
三次握手
- 流程图
-
使用netstat工具查看状态变化(参考图3-8)
四次挥手
-
流程图
-
使用netstat命令查看状态(参考图3-8)
TIME_WAIT的作用
在图3-8中,当客户端连接在收到服务器的结束报文段之后,并没有直接进人CLOSED 状态,而是转移到 TIME_WAIT 状态。在这个状态,客户端连接要等待段长为2MSL(Maximum Segment Life,报文段最大生存时间)的时间,才能完全关闭;MSL是 TCP 报文段在网络中的最大生存时间,标准文档 RFC 1122 的建议值是2 min;
TIME WAIT 状态存在的原因有两点:
-
可靠地终止TCP 连接
当服务器发给客户端的ACK中途丢失,客户端收不到ACK,会重新发送FIN,如果此时服务器已经关闭,无法接收来自客户端的FIN,便会陷入一种“藕断丝连”状态(一方关闭,一方未关闭)。这显然是不合适的,因为TCP 连接是全双工的,双方完成数据交换之后,通信双方都必须断开连接以释放系统资源。
-
保证让迟来的TCP 报文段有足够的时间被识别并丢弃
在 Linux 系统上,一个TCP 端口不能被同时打开多次(两次及以上)。当一个TCP 连接处于 TIME_WAIT 状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接。反过来,如果不存在 TIME WAIT 态,则应用序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具有相同的 IP 地址和端口号)。这个新的、和原来相似的连接被称为原来的连接的化身 (incarmation)。新的化身可能接收到属于原来的连接的、携带应用程序数据的 TCP 报文段(迟到的报文段),这显然是不应该发生的。这就是 TIMEWAIT 状态存在的第二个原因。
TCP协议特点
流式服务
TCP 字节流的特点,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,应用程序对数据的发送和接收是没有边界限制的。如下图:
TCP连接的可靠性
- IPV4报文格式:
- TCP报文格式:
- 应答机制
- 超时重传
TCP 传输是可靠的。首先,TCP 协议采用发送应答机制,即发送端发送的每个 TCP 报文段都必须得到接收方的应答,才认为这个 TCP 报文段传输成功。其次,TCP 协议采用超时重传机制,发送端在发送出1个 TCP 报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。最后,因为 TCP 报文段最终是以 IP数据报发送的,而 数据报到达接收端可能乱序、重复,所以 TCP 协议还会对接收到的 TCP 报文段重排、整理,再交付给应用层。
粘包问题
在流式服务中如上图3-9所示,尽管报文已经按顺序整理好并接受,但是无法分割成正确的信息,就形成了所谓的粘包问题,为了解决此问题,我们可以每次发送时进行标记分割,以便于接收方进行分析和拆分,如下图: