五十四、linux 编程——TCP 编程模型
54.1 编程模型介绍
54.1.1 TCP 客户端服务器编程模型
- 客户端调用序列
- 调用 socket 函数创建套接字
- 调用 connect 连接服务器端
- 调用 I/O 函数(read/write) 与服务器端通讯
- 调用 close 关闭套接字
- 服务器端调用序列
- 调用 socket 函数创建套接字
- 调用 bind 绑定本地地址和端口
- 调用 listen 启动监听
- 调用 accept 从已连接队列中提取客户连接
- 调用 I/O 函数(read/write)与客户端通讯
- 调用 close 关闭套接字
54.1.2 套接字与地址绑定
sockaddr 为自定义的结构体,示例如下:
(1)绑定地址
- 函数返回值:成功,则返回 0;出错,则返回 -1
(2)查找绑定到套接字的地址
- 返回值:成功,则返回 0;出错,则返回 -1
(3)获取对方地址
- 返回值:成功,则返回 0;出错, 则返回 -1
(4)建立连接
服务器端:
- 返回:成功返回0;出错返回 -1.
- 说明:backlog 指定进行客户端连接排队的队列长度
- 函数功能:获取客户端的连接
- 函数参数:
- address:通用地址,可以存放来源于客户端的地址信息,若不想获取客户端的信息,设置为NULL
- 返回值:
客户端:
- 返回:成功返回0;出错返回 -1
54.1.3 特殊 bind 地址
- 一台主机可以有多个网络接口和多个 IP 地址,如果我们只关心某个地址的连接请求,我们可以指定一个具体的本地 IP 地址,如果要响应所有接口上的连接请求,就要使用一个特殊的地址 INADDR_ANY
- #define INADDR_ANY (uint32_t)0x00000000
54.2 TCP 编程例子
客户端连接到服务器端后,服务器端返回给客户端一个系统时间,客户端将此时间打印出来
54.2.1 服务器端编程
time_tcp_server.c
1 #include <netdb.h> 2 #include <netinet/in.h> 3 #include <sys/socket.h> 4 #include <unistd.h> 5 #include <string.h> 6 #include <stdio.h> 7 #include <stdlib.h> 8 #include <memory.h> 9 #include <signal.h> 10 #include <time.h> 11 #include <arpa/inet.h> 12 13 14 int sockfd; 15 16 void sig_handler(int signo) 17 { 18 if(signo == SIGINT){ 19 printf("server close\n"); 20 /** 步骤6: 关闭 socket */ 21 close(sockfd); 22 exit(1); 23 } 24 } 25 26 /** 输出连接上来的客户端相关信息 */ 27 void out_addr(struct sockaddr_in *clientaddr) 28 { 29 /** 将端口从网络字节序转换成主机字节序 */ 30 int port = ntohs(clientaddr->sin_port); 31 char ip[16]; 32 memset(ip, 0, sizeof(ip)); 33 /** 将 ip 地址从网络字节序转换成点分十进制 */ 34 inet_ntop(AF_INET, &clientaddr->sin_addr.s_addr, ip, sizeof(ip)); 35 printf("client: %s(%d) connected\n", ip, port); 36 } 37 38 void do_service(int fd) 39 { 40 /** 获得系统时间 */ 41 long t = time(0); 42 char *s = ctime(&t); 43 ssize_t size = strlen(s) * sizeof(char); 44 45 /** 将服务器获得的系统时间写回到客户端 */ 46 if(write(fd, s, size) != size){ 47 perror("write error"); 48 } 49 } 50 51 int main(int argc, char *argv[]) 52 { 53 if(argc < 2){ 54 printf("usage: %s #port\n", argv[0]); 55 exit(1); 56 } 57 58 if(signal(SIGINT, sig_handler) == SIG_ERR){ 59 perror("signal sigint error"); 60 exit(1); 61 } 62 63 /** 步骤1: 创建 socket(套接字) 64 * 注: socket 创建在内核中,是一个结构体. 65 * AF_INET: IPV4 66 * SOCK_STREAM: tcp 协议 67 * AF_INET6: IPV6 68 */ 69 sockfd = socket(AF_INET, SOCK_STREAM, 0); 70 71 /** 72 * 步骤2: 调用 bind 函数将 socket 和地址(包括 ip、port)进行绑定 73 */ 74 struct sockaddr_in serveraddr; 75 memset(&serveraddr, 0, sizeof(struct sockaddr_in)); 76 /** 往地址中填入 ip、port、internet 地址族类型 */ 77 serveraddr.sin_family = AF_INET; ///< IPV4 78 serveraddr.sin_port = htons(atoi(argv[1])); ///< 填入端口 79 serveraddr.sin_addr.s_addr = INADDR_ANY; ///< 填入 IP 地址 80 if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in))){ 81 perror("bind error"); 82 exit(1); 83 } 84 85 /** 86 * 步骤3: 调用 listen 函数启动监听(指定 port 监听) 87 * 通知系统去接受来自客户端的连接请求 88 * (将接受到的客户端连接请求放置到对应的队列中) 89 * 第二个参数: 指定队列的长度 90 */ 91 if(listen(sockfd, 10) < 0){ 92 perror("listen error"); 93 exit(1); 94 } 95 96 /** 97 * 步骤4: 调用 accept 函数从队列中获得一个客户端的请求连接, 并返回新的 98 * socket 描述符 99 * 注意: 若没有客户端连接,调用此函数后会足则, 直到获得一个客户端的连接 100 */ 101 struct sockaddr_in clientaddr; 102 socklen_t clientaddr_len = sizeof(clientaddr); 103 while(1){ 104 int fd = accept(sockfd, (struct sockaddr *)&clientaddr, &clientaddr_len); 105 if(fd < 0){ 106 perror("accept error"); 107 continue; 108 } 109 110 /** 111 * 步骤5: 调用 IO 函数(read/write)和连接的客户端进行双向的通信 112 */ 113 out_addr(&clientaddr); 114 do_service(fd); 115 116 /** 步骤6: 关闭 socket */ 117 close(fd); 118 } 119 120 return 0; 121 }
编译测试:
可以看到,另一个终端返回了系统时间。
54.2.2 客户端编程
time_tcp_client.c
1 #include <sys/types.h> 2 #include <stdlib.h> 3 #include <stdio.h> 4 #include <memory.h> 5 #include <unistd.h> 6 #include <sys/socket.h> 7 #include <netdb.h> 8 #include <signal.h> 9 #include <string.h> 10 #include <time.h> 11 #include <arpa/inet.h> 12 13 14 int main(int argc, char *argv[]) 15 { 16 if(argc < 3){ 17 printf("usage: %s ip port\n", argv[0]); 18 exit(1); 19 } 20 21 /** 步骤1: 创建 socket */ 22 int sockfd = socket(AF_INET, SOCK_STREAM, 0); 23 if(sockfd < 0){ 24 perror("socket error"); 25 exit(1); 26 } 27 28 /** 往 serveraddr 中填入 ip、port 和地址族类型(ipv4) */ 29 struct sockaddr_in serveraddr; 30 memset(&serveraddr, 0, sizeof(struct sockaddr_in)); 31 serveraddr.sin_family = AF_INET; 32 serveraddr.sin_port = htons(atoi(argv[2])); 33 /** 将 ip 地址转换成网络字节序后填入 serveraddr 中 */ 34 inet_pton(AF_INET, argv[1], &serveraddr.sin_addr.s_addr); 35 36 /** 37 * 步骤2: 客户端调用 connect 函数连接到服务器端 38 */ 39 if(connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in)) < 0){ 40 perror("connect error"); 41 exit(1); 42 } 43 44 /** 步骤3: 调用 IO 函数(read/write)和服务器端进行双向通信 */ 45 char buffer[1024]; 46 memset(buffer, 0, sizeof(buffer)); 47 ssize_t size; 48 if((size = read(sockfd, buffer, sizeof(buffer))) < 0){ 49 perror("read error"); 50 } 51 if(write(STDIN_FILENO, buffer, size) != size){ 52 perror("write error"); 53 } 54 55 /** 步骤4: 关闭 socket */ 56 close(sockfd); 57 58 return 0; 59 }
编译在两个终端上,一个打开服务器,一个打开客户端测试: