OSI七层网络模型 先了解网络模型,更有利于后续学习和理解。
七层网络模型,实际上可以简化到四层(TCP/IP模型)。
(图片来源:C语言中文网-socket ,如果是从基础学习,推荐看看大佬的文章)
需要注意的是,数据只能逐层传输不能跃层,每一层都可以使用下层提供的服务,并为上层提供服务。
了解这个之后,我们对开发中不理解的地方,先搞清楚数据传输经历了哪些层或许更好理解为什么代码那么写。
TCP/IP协议族 上面图其实已经标注了TCP/IP模型包含四层,所谓简化就是简化成TCP/IP模型。
TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,其中 TCP 和 IP 是最常用的两种底层协议,所以把它们统称为“TCP/IP 协议族”。
注意,是“TCP/IP模型”包含很多协议,我们把这些协议统称为“TCP/IP协议族”。
本文记录socket编程也是基于TCP和UDP协议的,说“也是”,是因为大部分文章,大部分人都是从TCP/UDP记录学习的。
TCP和UDP协议的层级关系如下
socket套接字类型 面向连接的套接字和无连接的套接字,专业点叫流格式套接字(SOCK_STREAM)和数据报格式套接字(SOCK_DGRAM)。
SOCK_STREAM,底层基于TCP协议,提供一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
SOCK_DGRAM,底层基于UDP协议,是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。
对比维度
面向连接(TCP)
无连接(UDP)
连接建立
必须先三次握手建立专属连接,通信结束四次挥手断开
无需建立连接,直接发数据包,随时收发
可靠性
可靠:丢包重传、差错校验、应答确认
不可靠:不保证送达、不纠错、丢包不重发
数据有序性
保证按发送顺序到达,无乱序
可能乱序、重复,数据包独立无关联
数据形式
字节流(无数据边界,会粘包)
独立数据报(有边界,一次发送对应一次接收)
拥塞 / 流量控制
自带拥塞控制、滑动窗口流量控制
无任何控制,发送过快会直接丢包
头部开销
TCP 头部大(20~60 字节),开销高
UDP 头部极小(8 字节),转发快、延迟低
通信模式
一对一点对点通信
支持一对一、广播、组播
编程流程
复杂:listen/accept/connect 缺一不可
简单:无需监听、连接,直接 sendto/recvfrom
PS: Linux 下可以用read/recv,Windows 下使用recv/recvfrom
TCP 基于字节流传输,没有数据边界,会出现粘包(多包合并)、拆包(一包拆分)。简单解决思路:
固定长度:收发约定相同消息长度
分隔符结尾:用特殊符号标记消息结束
包头 + 长度:消息前加长度字段,按长度读取
面向连接(TCP)流程
服务端: socket() → bind() → listen() → accept() → send()/recv() → close()
客户端: socket() → connect() → send()/recv() → close()
无连接(UDP)流程
服务端 / 客户端通用: socket() → bind(可选) → sendto()/recvfrom()
IP、MAC和端口号 公网 IP 可以定位到一台主机(或 NAT 网关),但内网 IP 需要结合 NAT 才能唯一标识终端。在互联网上跑数据的IP都是公网IP,能定位到一个路由器,但一个路由器下还有局域网,并不能确定数据报来自哪个用户。
只有MAC地址能唯一确定一台计算机(网卡),每张网卡的MAC都是独一无二的,虽然也有修改MAC地址的技术手段,但我们讨论正常情况。
端口号是网络程序的网络接口。前面说了MAC能确定计算机,那还要精确到数据报来自计算机上的哪个网络程序,就要看端口号了,系统会为每个开启的网络应用分配一个端口号,我们自己写网络应用也可以指定端口号,但需要注意0-1024 是知名端口,避开他们。
有IP地址、MAC地址和端口号,才能唯一确定是哪个网络应用要通信。
阻塞与唤醒 我之所以用阻塞与唤醒的标题,是因为这里的阻塞实际上是休眠。
以前我只知道阻塞是应用程序看起来 卡住,之后触发什么条件从而继续运行的这么一个行为表现,但实际上这个话题比我想象的高深得多。
首先说明,我们可以用 fcntl/ioctl 把 Socket 设置为非阻塞模式,但默认情况我们socket编程处于阻塞模式。
当我们编写基于tcp的服务端时,调用 accept 就会阻塞,我好奇过系统是怎么知道我客户端发了消息服务端就知道继续往下执行的,一开始我以为是accept内部在轮询检查,不断循环检查是不是有消息,这样子处理的,但实际上,我的想法过于幼稚。
阻塞调用(accept/recv/read/epoll_wait)会把当前线程彻底移出 CPU 就绪 / 运行任务队列,进入休眠 等待队列,完全不占用CPU资源;这实际上对了解操作系统的开发者而言很容易想到,只在应用程序层面考虑可能不会太深。
既然是休眠,那与之对应的就是唤醒 了。唤醒靠的是硬件中断+事件驱动,当客户端发送信息之后,服务端网卡收到信号触发硬件中断,把消息写入内核缓冲区,接着内核标fd就绪 或把fd放进epoll就绪链表 ,再之后内核把之前休眠挂起的服务端线程,重新丢回 CPU 执行调度队列,最后,线程分到 CPU 时间片 → 阻塞函数返回,业务代码接着跑。
epoll 的事件通知机制与 select 不同,前者在内核维护就绪链表,后者是每次扫描(在后面的示例有所体现),所以epoll比select更高效;阻塞 I/O 时进程状态为 TASK_INTERRUPTIBLE(可中断睡眠),可以被信号唤醒。
客户端与服务端示例(Linux && TCP) 客户端由于是通用的就不过多解释,主要解释服务端。
把服务端 = 奶茶店店员,客户端 = 客人
单线程阻塞 :店员一次只给一个客人做奶茶,其他人站在门口等死,后面的人根本进不来(玩具项目,只能连接一个客户端);
多线程 :来一个客人雇一个店员,店里挤满店员,工资(内存)付不起,店直接倒闭(开辟线程服务,本质还是一对一,占用资源);
select/epoll :一个店员盯着所有客人,谁举手说「我要下单 / 我要取餐」,店员就立刻过去服务,效率拉满(内核调度)。
总结一下
不用 select/epoll : 服务端要么只能连 1 个客户端,要么线程太多把服务器干崩;用 select/epoll : 单线程同时管理海量连接,实现高并发;
用最低的资源,处理最多的客户端,这就是网络服务器的核心。
下面看看echo服务端和客户端的具体实现,客户端发送消息给服务端,服务端原封不动把客户端发送的消息返回。
通用客户端 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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 8888 #define BUFFER_SIZE 1024 int main () { int sock = 0 ; struct sockaddr_in serv_addr ; char send_buffer[BUFFER_SIZE] = {0 }; char buffer[BUFFER_SIZE] = {0 }; if ((sock = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { printf ("socket 创建失败\n" ); return -1 ; } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); if (inet_pton(AF_INET, "127.0.0.1" , &serv_addr.sin_addr) <= 0 ) { printf ("地址无效\n" ); return -1 ; } if (connect(sock, (struct sockaddr *)&serv_addr, sizeof (serv_addr)) < 0 ) { printf ("连接失败\n" ); return -1 ; } printf ("成功连接服务端!输入消息发送,输入 exit 退出\n" ); while (1 ) { memset (send_buffer, 0 , BUFFER_SIZE); memset (buffer, 0 , BUFFER_SIZE); printf ("请输入消息:" ); fgets(send_buffer, BUFFER_SIZE, stdin ); send_buffer[strcspn (send_buffer, "\n" )] = '\0' ; if (strcmp (send_buffer, "exit" ) == 0 ) { printf ("客户端退出\n" ); break ; } send(sock, send_buffer, strlen (send_buffer), 0 ); int len = read(sock, buffer, BUFFER_SIZE); if (len <= 0 ) { printf ("服务端断开连接\n" ); break ; } printf ("服务端回显: %s\n" , buffer); } close(sock); return 0 ; }
单连接服务端(玩具) 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 72 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 8888 #define BUFFER_SIZE 1024 int main () { int server_fd, client_fd; struct sockaddr_in serv_addr ; char buffer[BUFFER_SIZE] = {0 }; int addr_len = sizeof (serv_addr); if ((server_fd = socket(AF_INET, SOCK_STREAM, 0 )) == 0 ) { perror("socket 创建失败" ); exit (EXIT_FAILURE); } int opt = 1 ; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)) < 0 ) { perror("绑定失败" ); exit (EXIT_FAILURE); } if (listen(server_fd, 3 ) < 0 ) { perror("监听失败" ); exit (EXIT_FAILURE); } printf ("服务端启动成功,监听端口 %d,等待客户端连接...\n" , PORT); if ((client_fd = accept(server_fd, (struct sockaddr *)&serv_addr, (socklen_t *)&addr_len)) < 0 ) { perror("接收连接失败" ); exit (EXIT_FAILURE); } printf ("客户端已连接!\n" ); while (1 ) { memset (buffer, 0 , BUFFER_SIZE); int len = read(client_fd, buffer, BUFFER_SIZE); if (len <= 0 ) { printf ("客户端断开连接\n" ); break ; } printf ("收到客户端消息: %s\n" , buffer); send(client_fd, buffer, strlen (buffer), 0 ); } close(client_fd); close(server_fd); return 0 ; }
IO复用多连接服务端(select) 使用select,老式方案。
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/select.h> #define PORT 8888 #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int main (int argc, char * argv[]) { int server_fd, new_socket, client_socket[MAX_CLIENTS], max_sd, sd, activity, valread; struct sockaddr_in address ; fd_set readfds; char buffer[BUFFER_SIZE]; for (int i = 0 ; i < MAX_CLIENTS; ++i) client_socket[i] = 0 ; if ((server_fd = socket(AF_INET, SOCK_STREAM, 0 )) == 0 ) { perror("socket failed" ); exit (1 ); } int opt = 1 ; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (address)); address.sin_family = AF_INET; address.sin_port = htons(PORT); address.sin_addr.s_addr = INADDR_ANY; bind(server_fd, (struct sockaddr*)&address, sizeof (address)); listen(server_fd, 3 ); printf ("服务器监听端口 %d\n" , PORT); while (1 ) { FD_ZERO(&readfds); FD_SET(server_fd, &readfds); max_sd = server_fd; for (int i = 0 ; i < MAX_CLIENTS; ++i) { sd = client_socket[i]; if (sd > 0 ) FD_SET(sd, &readfds); if (sd > max_sd) max_sd = sd; } activity = select(max_sd + 1 , &readfds, NULL , NULL , NULL ); if (activity < 0 ) { perror("select error" ); } if (FD_ISSET(server_fd, &readfds)) { int addrlen = sizeof (address); new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t *)&addrlen); printf ("新连接: fd %d, IP %s, 端口 %d\n" , new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port)); for (int i = 0 ; i < MAX_CLIENTS; ++i) { if (client_socket[i] == 0 ) { client_socket[i] = new_socket; break ; } } } for (int i = 0 ; i < MAX_CLIENTS; ++i) { sd = client_socket[i]; if (FD_ISSET(sd, &readfds)) { valread = read(sd, buffer, BUFFER_SIZE); if (valread == 0 ) { struct sockaddr_in dc_address ; int dc_addrlen = sizeof (dc_address); getpeername(sd, (struct sockaddr*)&dc_address, (socklen_t *)&dc_addrlen); printf ("客户端断开:IP %s, 端口 %d\n" , inet_ntoa(dc_address.sin_addr), ntohs(dc_address.sin_port)); close(sd); client_socket[i] = 0 ; } else { buffer[valread] = '\0' ; printf ("收到:%s\n" , buffer); send(sd, buffer, strlen (buffer), 0 ); } } } } return 0 ; }
IO复用多连接服务端(epoll) epoll是Linux专属的,在Windows中有另一种机制,IOCP,都是实现高并发服务的必学机制。
epoll分水平触发(LT,默认)和边缘触发(ET),意思如同单片机引脚信号是稳定电平一直触发,还是上升沿/下降沿触发,所以二者处理事务的具体实现也不一样 。
水平触发因为一直会有事件通知,所以照常收发数据就好,边缘触发在客户端发数据到服务端来的时候,只会调用一次通知,所以需要循环读,直到读到EAGAIN ,表示数据收干净了。
先展示水平触发(LT)的具体实现。
注意,创建epoll实例:
epoll_create(size):旧版接口,size 参数已废弃
epoll_create1(0):推荐使用,现代 Linux 标准写法,参数传0即可
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/epoll.h> #include <sys/socket.h> #include <arpa/inet.h> #define PORT 8888 #define MAX_EVENTS 1024 int main (int argc, char * argv[]) { int server_fd = socket(AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); bind(server_fd, (struct sockaddr*)&addr, sizeof (addr)); listen(server_fd, 128 ); int epoll_fd = epoll_create1(0 ); struct epoll_event event ; event.events = EPOLLIN; event.data.fd = server_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event); struct epoll_event events [MAX_EVENTS ]; while (1 ) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1 ); for (int i = 0 ; i < n; ++i) { int fd = events[i].data.fd; if (fd == server_fd) { struct sockaddr_in client_addr ; socklen_t len = sizeof (client_addr); int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len); event.events = EPOLLIN; event.data.fd = client_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event); printf ("新客户端连接:fd %d\n" , client_fd); } else { char buf[1024 ]; int n = read(fd, buf, sizeof (buf)); if (n <= 0 ) { close(fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL ); printf ("客户端断开:%d\n" , fd); } else { buf[n] = '\0' ; printf ("收到:%s\n" , buf); send(fd, buf, n, 0 ); } } } } return 0 ; }
下面是边缘触发(ET)的具体实现,可以看到在处理新连接和处理接收消息时都放在了循环while(1) {...}内,因为边缘触发的信号只会触发一次,有新连接来的时候,有一次信号,倘若此时前面客户端的连接尚未完成,他会进入等待连接的队列中,也不会再有信号了,所以我们需要循环accept,直到所有客户端完成连接;同理,收取消息也是一样,消息来的时候只会有一个信号,但我们可能没法一次收取完整的消息,剩余数据就会一直堵在缓冲区里。
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/epoll.h> #include <sys/socket.h> #include <arpa/inet.h> #define PORT 8888 #define MAX_EVENTS 1024 void set_nonblocking (int fd) { int flag = fcntl(fd, F_GETFL, 0 ); fcntl(fd, F_SETFL, flag | O_NONBLOCK); } int main (int argc, char * argv[]) { int server_fd = socket(AF_INET, SOCK_STREAM, 0 ); if (server_fd == -1 ) { perror("socket failed" ); return -1 ; } int opt = 1 ; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr*)&addr, sizeof (addr)) == -1 ) { perror("bind failed" ); close(server_fd); return -1 ; } if (listen(server_fd, 128 ) == -1 ) { perror("listen failed" ); close(server_fd); return -1 ; } int epoll_fd = epoll_create1(0 ); if (epoll_fd == -1 ) { perror("epoll_create1 failed" ); close(server_fd); return -1 ; } set_nonblocking(server_fd); struct epoll_event event ; event.events = EPOLLIN | EPOLLET; event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1 ) { perror("epoll_ctl add server failed" ); close(server_fd); close(epoll_fd); return -1 ; } struct epoll_event events [MAX_EVENTS ]; printf ("Epoll ET 服务端启动成功,监听端口 %d\n" , PORT); while (1 ) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1 ); if (n == -1 ) { perror("epoll_wait failed" ); continue ; } for (int i = 0 ; i < n; ++i) { int fd = events[i].data.fd; if (fd == server_fd) { while (1 ) { struct sockaddr_in client_addr ; socklen_t len = sizeof (client_addr); int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len); if (client_fd == -1 ) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break ; } else { perror("accept failed" ); break ; } } printf ("新客户端连接:fd %d\n" , client_fd); set_nonblocking(client_fd); event.events = EPOLLIN | EPOLLET; event.data.fd = client_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event); } } else { char buf[1024 ]; while (1 ) { memset (buf, 0 , sizeof (buf)); ssize_t read_len = read(fd, buf, sizeof (buf)); if (read_len > 0 ) { printf ("收到客户端%d数据:%s\n" , fd, buf); send(fd, buf, read_len, 0 ); } else if (read_len == 0 ) { printf ("客户端%d断开连接\n" , fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL ); close(fd); break ; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { break ; } else { perror("read error" ); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL ); close(fd); break ; } } } } } } close(server_fd); close(epoll_fd); return 0 ; }
客户端与服务端示例(Linux && UDP) 因为 UDP没有请求连接和受理连接(没有connect、listen和accept) 的过程,所以严格意义上来说没有办法区分服务器端和客户端,在这里只能将提供服务的称为服务器端
udp发送的每个数据包自带目标ip和端口号,使用sendto/recvfrom通信,如果在sendto之前不bind地址,第一次调用 sendto 时,内核自动做隐式Bind ,内核根据路由表,自动选一个本机出口IP当源IP,自动分配一个系统临时随机端口当源端口,之后每次sendto都用这个目标地址和端口。
服务端 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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 32 void error_handling (char * message) ;int main (int argc, char ** argv) { int serv_sock; char message[BUF_SIZE]; int str_len; struct sockaddr_in serv_addr , clnt_addr ; socklen_t clnt_addr_size; if (argc != 2 ) { printf ("Usage: %s <port>\n" , argv[0 ]); exit (1 ); } serv_sock = socket(PF_INET, SOCK_DGRAM, 0 ); if (serv_sock == -1 ) error_handling("UDP socket creation error!" ); memset (&serv_addr, 0 , sizeof (serv_addr)); serv_addr.sin_family = PF_INET; serv_addr.sin_port = htons(atoi(argv[1 ])); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof (serv_addr)) == -1 ) error_handling("bind() error!" ); while (1 ) { clnt_addr_size = sizeof (clnt_addr); str_len = recvfrom(serv_sock, message, BUF_SIZE, 0 , (struct sockaddr*)&clnt_addr, &clnt_addr_size); sendto(serv_sock, message, str_len, 0 , (struct sockaddr*)&clnt_addr, clnt_addr_size); } close(serv_sock); return 0 ; } void error_handling (char * message) { fputs (message, stderr ); fputc('\n' , stderr ); exit (1 ); }
客户端 // udp_client_echo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // write read 声明的地方
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 32
void error_handling(char* message);
int main(int argc, char** argv) {
int clnt_sock;
char message[BUF_SIZE];
int str_len;
socklen_t addr_size;
struct sockaddr_in serv_addr, from_addr;
if (argc != 3) {
printf("Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
clnt_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (clnt_sock == -1)
error_handling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = PF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) == 0) // 会自动转换字节序
error_handling("inet_pton() error!");
while(1) {
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;
sendto (clnt_sock, message, strlen(message), 0,
(struct sockaddr*)&serv_addr, sizeof(serv_addr));
addr_size = sizeof(serv_addr);
str_len = recvfrom(clnt_sock, message, BUF_SIZE, 0,
(struct sockaddr*)&from_addr, &addr_size);
message[str_len] = 0;
printf("Message from server: %s\n", message);
}
close(clnt_sock);
return 0;
}
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}