一、什么是TCP协议
TCP(Transmission Control Protocol)协议是一种面向连接的、可靠的、基于字节流的传输控制协议,属于传输层。TCP协议可以通过错误检测、重传丢失的数据包、流量控制、拥塞控制等方式来实现可靠传输,同时也具有较好的效率和速度。
二、特性
1、面向连接:TCP协议是一种面向连接的协议,需要在数据传输前先建立连接,传输完成后再释放连接。
2、 可靠传输:TCP协议通过序号和确认机制来实现可靠传输,确保数据在网络中被正确无误地传输。
3、流量控制:TCP协议通过窗口机制来实现流量控制,避免发送方发送过多数据导致接收方不堪重负。
4、拥塞控制:TCP协议通过拥塞窗口控制、快速重传和快速恢复等机制来实现拥塞控制,避免网络拥塞导致数据传输的延迟和丢失。
5、面向字节流:TCP协议是一种面向字节流的协议,数据被视为一连串的字节流进行传输。
6、可以提供全双工传输:TCP协议可以同时接受发送方和接收方的数据传输,实现全双工传输。
7、支持多路复用:TCP协议可以通过端口号来实现多路复用,使得多个应用程序可以同时使用同一个网络连接。
三、使用场景
1、 网页浏览:网页浏览器使用TCP协议与Web服务器建立连接,以可靠地传输HTTP请求和响应数据。
2、电子邮件传输:电子邮件客户端和邮件服务器之间的传输也使用TCP协议,以确保邮件的正确传输和接收。
3、文件传输:文件传输协议(FTP)和远程拷贝协议(SCP)等协议均使用TCP协议进行可靠传输。
4、数据库访问:数据库客户端通过TCP协议与数据库服务器建立连接,以进行数据的可靠读写操作(例如注册或者登录账号)。
5、远程登录:远程登录协议(例如SSH)使用TCP协议进行可靠传输和安全身份验证。
6、实时通信:实时通信协议(例如视频会议、语音聊天等,但一般使用UDP协议的多)也可以使用TCP协议进行可靠的数据传输。
总之,任何需要可靠传输和连接保持的应用场景都可以使用TCP协议进行数据传输。
四、C/S架构TCP通信流程
1、客户端
(1)、建立套接字。使用socket()
(2)、设置端口复用(可选,推荐)。使用setsockopt()
(3)、绑定自己的IP地址和端口号(可以省略)。使用bind()
(4)、向服务器发起连接请求。使用connect(),相当于第一个握手
(5)、成功连接后可以进入收发数据。使用send()或者write()发送数据,recv()或者read()接收数据
(6)、关闭套接字。使用close()
2、服务端
(1)、建立套接字。使用socket()
(2)、设置端口复用(可选,推荐)。使用setsockopt()
(3)、绑定自己的IP地址和端口号(不可以省略)。使用bind()
(4)、设置监听。使用listen()
(5)、接受连接请求。使用accept()
(6)、成功连接后可以进入收发数据。使用send()或者write()发送数据,recv()或者read()接收数据
(7)、关闭套接字。使用close()
五、相关的函数API
1、建立套接字
// 建立套接字 int socket(int domain, int type, int protocol); // 接口说明 返回值:成功返回一个套接字文件描述符,失败返回-1 参数domain:用来指定使用何种地址类型,有很多,具体看别的资源 (1)PF_INET 或者 AF_INET 使用IPV4网络协议 (2)其他很多的,看别的资源 参数type:通信状态类型选择,有很多,具体看别的资源 (1)SOCK_STREAM 提供双向连续且可信赖的数据流,即TCP (2)SOCK_DGRAM 使用不连续不可信赖的数据包连接,即UDP 参数protocol:用来指定socket所使用的传输协议编号,通常不用管,一般设为0
2、设置端口状态
// 设置端口的状态 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); // 接口说明 返回值:成功返回0,失败返回-1 参数sockfd:待设置的套接字 参数level: 待设置的网络层,一般设成为SOL_SOCKET以存取socket层 参数optname:待设置的选项,有很多种,具体看别的资源,这里讲常用的 (1)、SO_REUSEADDR 允许在bind()过程中本地地址可复用,即端口复用 (2)、SO_BROADCAST 使用广播的方式发送,通常用于UDP广播 (3)、SO_SNDBUF 设置发送的暂存区大小 (4)、SO_RCVBUF 设置接收的暂存区大小 参数optval:待设置的值 参数optlen:参数optval的大小,即sizeof(optval)
3、绑定自己的IP地址和端口号
// 绑定自己的IP地址和端口号 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 接口说明 返回值: 参数sockfd:待绑定的套接字 参数addrlen:参数addr的大小,即sizeof(addr) 参数addr:IP地址和端口的结构体,通用的结构体,根据sockfd的类型有不同的定义 当sockfd的domain参数指定为IPV4时,结构体定义为 struct sockaddr_in { unsigned short int sin_family; // 需与sockfd的domain参数一致 uint16_t sin_port; // 端口号 struct in_addr sin_addr; // IP地址 unsigned char sin_zero[8]; // 保留的,未使用 }; struct in_addr { uin32_t s_addr; } // 注意:网络通信时,采用大端字节序,所以端口号和IP地址需要调用专门的函数转换成网络字节序
4、字节序转换接口
// 第一组接口 // 主机转网络IP地址,输入主机IP地址 uint32_t htonl(uint32_t hostlong); // 主机转网络端口,输入主机端口号 uint16_t htons(uint16_t hostshort); // 常用 // 网络转主机IP,输入网络IP地址 uint32_t ntohl(uint32_t netlong); // 网络转主机端口,输入网络端口 uint16_t ntohs(uint16_t netshort); // 第二组接口,只能用于IPV4转换,IP地址 // 主机转网络 int inet_aton(const char *cp, struct in_addr *inp); // 主机转网络 in_addr_t inet_addr(const char *cp); // 常用 // 网络转主机 int_addr_t inet_network(const char *cp); // 网络转主机 char *inet_ntoa(struct in_addr in); // 常用
5、设置监听
// 设置监听 int listen(int sockfd, int backlog); // 接口说明 返回值:成功返回0,失败返回-1 参数sockfd:待监听的套接字 参数backlog:指定同时能出处理的最大连接请求数量 当sockfd的domain参数指定为AF_INET(IPV4)时,该参数backlog最大值为128
6、向服务器发起连接请求
// 向服务器发起连接请求 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 接口说明 返回值: 参数sockfd:成功返回0,失败返回-1 参数addr:服务器的网络地址 参数addrlen:参数addr的大小,即sizeof(addr)
7、接收连接请求
// 接受连接请求 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 接口说明 返回值:成功返回一个新连接上来的客户端套接字文件描述符,失败返回-1 参数sockfd:服务端套接字 参数addr:连接上来的客户端的网络地址 参数addrlen:参数addr的大小,即sizeof(addr)
8、发送数据
// 发送数据 ssize_t send(int sockfd, const void *buf, size_t len, int flags); // 接口说明 返回值:成功返回成功发送的字节数,失败返回-1 参数sockfd:发送者的文件描述符 参数buf:数据缓冲区 参数len:发送的数据大小 参数flags:有很多种,一般设置为0 // 发送数据,也可以使用这个 ssize_t write(int fd, const void *buf, size_t count); // 接口说明 返回值:成功返回成功发送的字节数,失败返回-1 参数fd:发送者的文件描述符 参数buf:数据缓冲区 参数count:发送的数据大小
9、接收数据
// 接收数据 ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 接口说明 返回值:成功返回实际接收的字节数,失败返回-1 如果返回0,有以下几种情况: (1)、对等端已经有序关闭,下线了(TCP协议中经常出现) (2)、对等端发送一个字节数为0的数据报 (3)、如果请求从流套接字接收的字节数为0,则也可能返回值0。 参数sockfd:接收者套接字 参数buf:数据缓冲区 参数len:最大可接受的字节数 参数flags:有很多种,一般设置为0 // 接收数据,也可以使用这个 ssize_t read(int fd, void *buf, size_t count); // 接口说明 返回值:成功返回实际接收的字节数,失败返回-1 如果返回0,对等端已经有序关闭,下线了(TCP协议中经常出现) 参数buf:数据缓冲区 参数len:最大可接受的字节数
10、关闭套接字
// 关闭套接字 int close(int fd); // 接口说明 返回值:成功返回0,失败返回-1 参数fd:套接字文件描述符
六、案例
使用TCP协议完成C/S架构的客户端和服务端通信演示
客户端TcpClient.c
// TCP客户端的案例 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <signal.h> #define CLIENT_IP "192.168.64.128" // 记得改为自己IP #define CLIENT_PORT 10000 // 不能超过65535,也不要低于1000,防止端口误用 // 自定义的退出信号响应函数 void exit_handler(int sig) { printf("[%d] exit\n", getpid()); exit(0); } int main(int argc, char *argv[]) { // 注册自定义退出信号响应函数 signal(34, exit_handler); // 1、建立套接字,指定IPV4网络地址,TCP协议 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd == -1) { perror("socket fail"); return -1; } // 2、设置端口复用(推荐) int optval = 1; // 这里设置为端口复用,所以随便写一个值 int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if(ret == -1) { perror("setsockopt fail"); close(sockfd); return -1; } // 3、绑定自己的IP地址和端口号(可以省略) struct sockaddr_in client_addr = {0}; socklen_t addr_len = sizeof(struct sockaddr); client_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议 client_addr.sin_port = htons(CLIENT_PORT); // 端口号 client_addr.sin_addr.s_addr = inet_addr(CLIENT_IP); // IP地址 ret = bind(sockfd, (struct sockaddr*)&client_addr, addr_len); if(ret == -1) { perror("bind fail"); close(sockfd); return -1; } // 4、向服务器发起连接请求 uint16_t port = 0; // 端口号 char ip[20] = {0}; // IP地址 struct sockaddr_in server_addr = {0}; printf("please input server IP and port:\n"); scanf("%s %hd", ip, &port); printf("IP = %s, port = %hd\n", ip, port); server_addr.sin_family = AF_INET; // 指定IPV4地址类型 server_addr.sin_port = htons(port); // 服务器端口号 server_addr.sin_addr.s_addr = inet_addr(ip); // 服务器IP地址 // 设置简单的超时重连 for(int i = 0; i < 10; i++) { // 向服务器发起连接请求,第一次握手 ret = connect(sockfd, (struct sockaddr*)&server_addr, addr_len); if(ret == -1) { perror("connect fail"); printf("try to connect after 1 second\n"); sleep(1); if(i == 10) { printf("server log out, please connect again later\n"); close(sockfd); return -1; } } else { printf("connect server success\n"); break; // 成功连接就退出 } } // 5、收发数据 char msg[128] = {0}; // 数据缓冲区 pid_t pid = fork(); // 父进程负责发送数据 if(pid > 0) { while(getchar() != '\n'); // 清空多余的换行符 while(1) { printf("please input data:\n"); fgets(msg, sizeof(msg)/sizeof(msg[0]), stdin); ret = send(sockfd, msg, strlen(msg), 0); if(ret > 0) { printf("success: send %d bytes\n", ret); } else { perror("send error"); } } } // 子进程负责接收数据 else if(pid == 0) { while(1) { ret = recv(sockfd, msg, sizeof(msg)/sizeof(msg[0]), 0); if(ret > 0) { printf("recv data: %s\n", msg); memset(msg, 0, sizeof(msg)); } // 服务器已经掉线,这里直接退出 else if(ret == 0) { printf("server log out\n"); close(sockfd); kill(getppid(), 34); // 给父进程发送退出信号 kill(getpid(), 34); // 给自己发送退出信号 } } } else { perror("fork fail"); close(sockfd); return -1; } return 0; }
服务端TcpServer.c
// TCP服务器的案例 #include <stdio.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <signal.h> #define MAX_LISTEN 50 // 最大能处理的连接数 #define SERVER_IP "192.168.64.128" // 记得改为自己IP #define SERVER_PORT 20000 // 不能超过65535,也不要低于1000,防止端口误用 // 自定义的退出信号响应函数 void exit_handler(int sig) { printf("[%d] exit\n", getpid()); exit(0); } int main(int argc, char *argv[]) { // 注册自定义退出信号响应函数 signal(34, exit_handler); // 1、建立套接字,指定IPV4网络地址,TCP协议 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd == -1) { perror("socket fail"); return -1; } // 2、设置端口复用(推荐) int optval = 1; // 这里设置为端口复用,所以随便写一个值 int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if(ret == -1) { perror("setsockopt fail"); close(sockfd); return -1; } // 3、绑定自己的IP地址和端口号(不可以省略) struct sockaddr_in client_addr = {0}; socklen_t addr_len = sizeof(struct sockaddr); client_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议 client_addr.sin_port = htons(SERVER_PORT); // 端口号 client_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址 ret = bind(sockfd, (struct sockaddr*)&client_addr, addr_len); if(ret == -1) { perror("bind fail"); close(sockfd); return -1; } // 4、设置监听 ret = listen(sockfd, MAX_LISTEN); if(ret == -1) { perror("listen fail"); close(sockfd); return -1; } // 5、接受连接请求 printf("wait client connect...\n"); uint16_t port = 0; char ip[20] = {0}; int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len); if(client_fd == -1) { perror("accept fail"); close(sockfd); return -1; } else { memset(ip, 0, sizeof(ip)); strcpy(ip, inet_ntoa(client_addr.sin_addr)); port = ntohs(client_addr.sin_port); printf("[%s:%d] connect\n", ip, port); } // 6、收发数据, 这里只做处理当个客户端的请求 char msg[128] = {0}; pid_t pid = fork(); // 父进程负责发送数据 if(pid > 0) { while(1) { printf("please input data:\n"); fgets(msg, sizeof(msg)/sizeof(msg[0]), stdin); // 注意套接字是客户端的套接字,不是服务器的 ret = send(client_fd, msg, strlen(msg), 0); if(ret > 0) { printf("success: send %d bytes\n", ret); } else { perror("send error"); } } } // 子进程负责接收数据 else if(pid == 0) { while(1) { ret = recv(client_fd, msg, sizeof(msg)/sizeof(msg[0]), 0); if(ret > 0) { printf("[%s:%d] send data: %s\n", ip, port, msg); memset(msg, 0, sizeof(msg)); // 清空数据区 } // 客户端断开连接 else if(ret == 0) { printf("[%s:%d] log out\n", ip, port); close(client_fd); close(sockfd); kill(getppid(), 34); // 给父进程发送退出信号 kill(getpid(), 34); // 给自己发送退出信号 } } } else { perror("fork fail"); close(sockfd); return -1; } // 7、关闭套接字 close(sockfd); return 0; }
通信演示
注:上述演示的服务器只能处理一个客户端连接,如果要处理多个可以看本系列中的服务器篇,在最开头有链接
七、总结
TCP(Transmission Control Protocol)协议是一种面向连接的、可靠的、基于字节流的传输控制协议,属于传输层,任何需要可靠传输和连接保持的应用场景都可以使用TCP协议进行数据传输。TCP协议下的客户端通信流程和服务器通信流程不完全一致,可以结合案例加深对TCP协议的理解。