TCP回射 服务端程序
本例为多进程的 TCP 回射程序(服务端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
#include <unp.h> int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; // 指定协议族(AF_INET: ipv4 协议), // 指定套接字类型(TCP 数据流, UDP 数据报或者为原始套接字SOCK_STREAM: 基于 tcp), // 最后一个选项默认即可 // 成功返回一个非负的套接字描述符(socket descriptor), 此时 listenfd 是一个将要 // 主动调用 connect 函数的客户端套接字(主动套接字) listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 初始化服务端套接字 bzero(&servaddr, sizeof(servaddr)); // 套接字规范中, 仅需要协议族, ip 地址和端口号三个属性 servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); // 本地协议地址赋予套接字(端口相关, 初步理解为进程赋予众所周知的端口) Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); // 将 listenfd 转换成被动套接字, 指示内核, 此套接字可被调用, 此时, 服务端监听开始! // 客户端理所应当的免去这一步 Listen(listenfd, LISTENQ); for ( ; ; ) { clilen = sizeof(cliaddr); // 返回已建立连接的客户端套接字, 如果当前没有套接字完成连接, 函数阻塞 // 本函数循环调用, 直到所有建立连接的客户端套接字服务完毕, 此时阻塞, // 等待新的连接建立. // 客户端的套接字被存放于 cliaddr 中. connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); // 核心!!! // 运行到此, 说明已经成功获得(否则阻塞与 accept 中) connfd, 但是本 // 主进程并不处理客户端连接, 而是通过 fork() 复制一个完全相同的子进程 // 服务客户套接字. // 服务端调用 fork() 时, 会返回一个非零的子进程 id, 故而, if 中的语句永远 // 不会被父进程执行, 而被复制而来的子进程运行到这里的时候, fork() 函数不会复制 // 新的子进程, 而是返回 0, 即, c 中, fork() 函数一次调用, 会有两次返回值. if ( (childpid = Fork()) == 0) { /* 子进程 */ // 子进程拿到 connfd 连接之后, // 先关闭监听套接字, 子进程无需监听, 全权交由父进程, 而后由父进程分发请求即可 Close(listenfd); str_echo(connfd); /* 处理请求 */ exit(0); // 子进程到此, 已完成使命, 终止退出即可, 此时 connfd 默认 close } Close(connfd); /* 父进程断开连接 */ } } void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while ( (n = read(sockfd, buf, MAXLINE)) > 0) Writen(sockfd, buf, n); if (n < 0 && errno == EINTR) goto again; else if (n < 0) err_sys("str_echo: read error"); } |
- 套接字 sockaddr_in cliaddr, servaddr
网际套接字地址结构, 包含协议族(ipv 4, ipv 6), 端口号, ip 地址
TCP回射 客户端程序
本例为多进程的 TCP 回射程序(客户端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; // 错误退出 if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); // 创建一个主动套接字, 和服务端协议一致即可 sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); // ip 格式转换 点分十进制和二进制整数 Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); // 本地套接字连接服务端套接字, 此时客户端会自行分配端口 Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); // 连接成功, 执行操作即可 str_cli(stdin, sockfd); /* 输入 */ exit(0); } void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } } |
在客户端程序中, str_cli 函数主要做两件事情:
- 从标准输入中获取数据并发送给服务端(Writen) -阻塞
- 从服务器中获取回射数据(Readline)并输出到标准输出 -阻塞
而本例中, 主要依赖于 Fgets 函数从标准输入获取数据, 如果用户长时间不输入文本, 则 1, 2 都被阻塞, 极大的影响了性能, 后续使用基于 select 和 epoll 的 I/O 复用模型将能改善此场景.
- 代码为 unix 网络编程 的示例代码