1.socket 通信1.1 大小端转换
- 主机字节序 16 位值 网络字节序 16 位值
- 主机字节序 32 位值 网络字节序 32 位值
#include // 主机字节序转换为网络字节序uint16_t htons(uint16_t hostshort); // host to net unsigned short 可用端口转换unit32_t htonl(unit32_t hostlong); // host to net unsigned int 可用ip地址转换// 网络字节序转换为主机字节序uint16_t ntohs(uint16_t netshort);unit32_t ntohl(unit32_t netlong);
1.2 IP地址转换
- 主机字节序的字符串IP地址 网络字节序的整形IP地址
#include // 主机字节序IP to 网络字节序(大端)IPint inet_pton(int af, const char* src, void* dst);/* 参数: af: 地址族协议 AF_INET(ipv4), AF_INET6(ipv6) src: 主机字节序的字符串类型的IP地址,被转换的数据 dst: 传出参数, 存储转换之后的大端的IP地址 返回值: 成功0; 失败-1 */const char *int_ntop(int af, const void *src, char *dst, socklen_t size);/* 参数: af: 地址族协议 AF_INET; AF_INET6 src: 传入参数, 要被转换的数据指针, 指向内存中存储的大端IP地址(整形数) dst: 传出参数, 指针指向主机字节序, 字符串类型的IP地址 size: dst指向的内存的大小 返回值: 成功: 返回指向 dst 指针指向的内存 失败: NULL */
1.3 套接字相关函数1.3.1 socket 创建
#include // 该头文件包括了 int socket(int domain, int type, int protocol);/* 参数: domain: AF_INET; AF_INET6 type: SOCK_STREAM: 流式传输协议 TCP SOCK_DGRAM: 报式传输协议 UDP protocol: 默认写0 流式传输默认 TCP 报式传输默认 UDP 返回值: 成功: 返回文件描述符 失败: 返回-1 */
1.3.2 bind 绑定套接字
将监听的套接字和本地IP和端口进行关联
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);/* 参数: sockfd: 用于监听的套接字, 通过socket创建 addr: 将本地ip和端口初始化给该结构体(需要用大端) 绑定的时候服务器一般ip使用宏 INADDR_ANY (0) 0 表示绑定该主机的所有ip地址, 多个网卡可能有多个ip addrlen: 记录第二个指针指向内存的大小, sizeof(struct sockaddr) 返回值: 成功0, 失败-1 */
1.3.3 listen 监听套接字
给监听的套接字设置监听,开始检测客户端链接
int listen(int sockfd, int backlog);/* 参数: sockfd: 监听的套接字, 设置监听前需要先绑定 backlog: 可以同时检测的新的连接个数, 最大值128 返回值: 成功0, 失败-1 */
1.3.4 accept 接收客户端连接
等待并接受客户端的连接,阻塞函数,没有客户端连接就阻塞,监听的文件描述符缓冲区没有数据就阻塞,有数据就解除阻塞建立连接,连接建立成功后,返回一个通信用的文件描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);/* 参数: sockfd: 监听的文件描述符 addr: 传出参数, 保存了建立连接的客户端的地址信息(ip 端口) -> 大端存储 不需要客户端信息则填NULL addrlen: 传入传出参数, 传入addr指针指向的内存大小, 传出存储了客户端信息的addr内存大小 addr为NULL,则该参数也填NULL 返回值: 文件描述符或-1 */
1.3.5 read、recv 读数据
读取数据,如果数据区空会读堵塞
ssize_t read(int sockfd, void *buf, size_t size);ssize_t recv(int sockfd, void *buf, size_t size, int flags);/* 参数: sockfd: 通信文件描述符 服务器端: accept 返回值 客户端: socket 创建得到, connect 初始化连接 buf: 存储接收到的数据, 数据来自文件描述符对应的缓冲区 size: buf 的内存容量 flag: 默认属性0即可 返回值: >0: 读到的字节数 =0: 对方断开连接 -1: 读异常, 失败 */
1.3.6 write、send 写数据
发送数据,如果数据区满会写阻塞
ssize_t write(int fd, const void *buf, size_t len);ssize_t send(int fd, const void *buf, size_t len, int flags);/* 参数: fd: 通信的文件描述符 buf: 要发送的数据缓冲区 len: 缓冲区大小 flags: 使用默认属性0即可 */
1.3.7 recvfrom / sendto 发送接收
- 报式传输协议发送
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);/* 参数: sockfd: 通信文件描述符 buf: 一块有效内存地址 len: 参数buf指向的内存地址大小 flags: 默认属性0即可 src_addr: 传出参数, 保存发送端的IP和端口(网络字节序), 不感兴趣可以NULL addrlen: 传入传出参数, src_addr指针指向内存空间的大小, 如果src_addr为NULL, 则填NULL 返回值: >0: 接收到的字节数; -1: 失败 */
- 报式传输协议接收
ssize_t sendto(int sockfd, void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t *addrlen);/* 参数: sockfd: 通信文件描述符 buf: 待发送的数据地址 len: 参数buf指向的内存地址大小 flags: 默认属性0即可 dest_addr: 传入参数, 接收端的IP和端口信息(网络字节序) addrlen: 传入参数, src_addr指针指向内存空间的大小 返回值: >0: 发送的字节数; -1: 失败 */
1.3.8 connect 客户端连接
客户端连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);/* 参数: sockfd: 通信文件描述符 addr: 连接服务器的ip和端口信息(需要使用大端描述) addrlen: 参数addr指向的内存大小 返回值: 成功0; 失败-1 */
1.4 套接字选项
该函数用来设置套接字选项,端口复用、广播、组播等,下面是端口复用的参数解释
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);/* 参数 sockfd: 监听的套接字 level: SOL_SOCKET optname: SO_REUSEPORT optval: 实际类型int 0 -> 端口不复用 1 -> 端口复用 optlen: optval 指针指向的内存大小 sizeof(int) 返回值 成功0, 失败-1 */
2. IO多路复用2.1 select
- 构造一个文件描述符列表,将要监听的文件描述符添加到该列表中(最大支持1024,线性描述)
- 读集合:检测文件描述符列表的读缓冲区
- 监听的文件描述符:新客户端连接
- 通信的文件描述符:新数据到达
- 写集合:内核检测集合中文件描述符是否可写
- 通信的文件描述符
- 异常集合:检测文件描述符是否有异常
- 写集合:内核检测集合中文件描述符是否可写
- 返回时,告诉进程有哪些描述符需要进行IO操作
调用一个函数,监听该表中的文件描述符,知道这些描述符中的一个进行IO操作时,函数返回(该函数为阻塞函数,检测由内核完成)
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);/* 参数: nfds: 下面三个集合中, 最大文件描述符值 + 1 readfds: 传出传出参数,读集合,检测若干文件描述符的读缓冲区(新连接 / 新数据) writefds: 传入传出参数,写集合,检测若干文件描述符的写缓冲区(一般都可写,很少用) execptfds: 传入传出参数,异常集合 timeout: 表示时间段,最长检测多长时间,超过这个时间还在阻塞就解除阻塞 NULL 一直阻塞等待; 0 函数调用后立刻返回 返回值: >0: 检测完成后,满足条件的总个数 =0: 超时强制返回 - 1: 失败 */
timeval 结构体
struct timeval { time_t tv-sec; suseconds_t tv_usec;};
fd_set 文件描述符集合(位操作)操作函数
void FD_CLR(int fd, fd_set *set); // 删除fdint FD_ISSET(int fd, fd_set *set); // 判断fd是否在集合void FD_SET(int fd, fd_set *set); // 添加fdvoid FD_ZERO(fd_set *set); // 清空fd(初始化)
2.2 epoll
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统。把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3)调用epoll_wait收集发生的事件的连接
如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
2.2.1 epoll_create 创建 epoll
#include int epoll_create(int size);/* 参数: size: 没有实际意义, 大于0即可 返回值: 成功: 返回一个文件描述符 该文件描述符对应的指针存储了红黑树的根节点 失败: -1 */
2.2.2 epoll_ctl 操作epoll
实现对 epoll 树上节点的操作(添加、修改、删除节点)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);/* 参数: epfd: epoll_create() 函数的返回值,找到对应的epoll实例 op: EPOLL_CTL_ADD: 添加新节点 EPOLL_CTL_MOD: 修改已经添加到树上节点的属性(读改写) EPOLL_CTL_DEL: 删除节点 fd: 要操作的文件描述符 添加 / 修改 / 删除(监听、通信) event: 对应的事件(若删除填NULL) EPOLLIN: 读事件 EPOLLOUT: 写事件 */
- epoll_data
typedef union epoll_data{ void *ptr; int fd; // 该联合体常用这个 uint32_t u32; uint64_t u64;} epoll_data_t;
- epoll_event
- event 是位操作,EPOLLIN 检测写缓冲区,EPOLLOUT 检测读缓冲区
- data.fd 等于 epoll_ctl 函数调用的第三个参数
struct epoll_event{ uint32_t event; // Epoll events; epoll_data_t data; // User data variable};
2.2.3 epoll_wait
阻塞函数,委托内核检测epoll树上文件描述符的状态,如果没有状态变化,默认一直阻塞
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);/* 参数: epfd: epoll_create() 的返回值, 找到epoll实例 event: 传出参数,记录了这轮检测到epoll模型中有状态变化的文件描述符(结构体数组地址) maxevent: events数组的容量 timeout: 超时时长 ms(-1一直阻塞; 0立即返回) 返回值: 成功: 有多少文件描述符发生变化 */
2.2.4 Level triggered 水平模式(默认)
LT(level triggered)是缺省的工作方式,同时支持 block 和 no-block socket。这种模式下,内核会通知文件描述符是否就绪,如果不进行任何操作,内核会一直通知你该文件描述符就绪
2.2.5 Edge triggered 边沿模式
ET(edge triggered)是高速工作模式,只支持 no-block socket。这种模式下,如果接到通知,但是没有把数据从缓冲区读完,epoll_wait不会再次通知;直到再次接收到新数据也一样通知一次,但是此时他会接着上次的缓冲区数据读。
struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; // 设置文件描述符为边沿模式 ev.data.fd = lfd;
使用边沿模式读数据需要在收到消息后我们一般需要 while(1) 死循环读取数据直到缓冲区数据读完,所以需要设置文件描述符为非阻塞状态,让read可以非阻塞读取数据,通过 read 的返回值判断是否结束该死循环
int fcntl(int fd, int cmd, ...);int flag = fcntl(cfd, F_GETFL);flag = flag | O_NONBLOCK;fcntl(cfd, F_SETFL, flag); //设置文件描述符为非阻塞, read函数再读取不会阻塞
最后因为这里已经设置为非阻塞,可以根据read的返回值判断是否已经读完缓冲区了,如果读完了会有errno EAGAIN的错误码,根据该错误码跳出循环即可
while(1){ int len = recv(curfd, buf, sizeof(buf), 0); if(len > 0) printf("打印接收的数据"); else if( len == 0) printf("断开连接"); else { if(errno==EAGAIN) { printf("数据读完了"); break; // 跳出循环 } perror("接收错误"); exit(0); }}
3. 代码示例3.1 TCP、epoll服务器
- 创建socket套接字
- 绑定ip和端口
- 设置监听
- 初始化一个epoll树
- 将文件描述符加入epoll树
- 委托内核检测文件描述符状态
#include #include #include #include <string.h>#include #include int main(){ // 1. 创建套接字 int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket error"); exit(1); } // 2.将 套接字 和 ip端口 绑定 struct sockaddr_in addr; addr.sin_family = AF_INET; // ipv4 addr.sin_addr.s_addr= INADDR_ANY; // 0地址(本地任意地址) addr.sin_port = htons(8989); // 端口转为大端 int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("bind error"); exit(2); } // 3.设置监听 ret = listen(lfd, 128); if(ret == -1) { perror("listen error"); exit(3); } // 4.初始化检测的集合 int epfd = epoll_create(1); if(epfd == -1) { perror("epoll_create error"); exit(4); } // 5.将要检测的节点添加到epoll树中 struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = lfd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); if(ret == -1) { perror("epoll_ctl"); exit(5); } // 6.委托内核检测epoll树中的文件描述符状态 struct epoll_event evs[1024]; int size = sizeof(evs) / sizeof(evs[0]); while(1) { int num = epoll_wait(epfd, evs, size, -1); // 把文件描述符发生变化的储存到 evs 数组中 printf("num = %d\n", num); // 遍历evs数组 for(int i=0; i<num; i++) { int curfd = evs[i].data.fd; if(curfd == lfd) // lfd 套接字状态改变说明有新链接请求 { int cfd = accept(lfd, NULL, NULL); ev.events = EPOLLIN; ev.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // 把新的链接加入到epoll树中 } else // 其他套接字状态改变说明有新数据抵达 { char buf[1024]; memset(buf, 0, sizeof(buf)); int len = recv(curfd, buf, sizeof(buf), 0); if(len == 0) { printf("客户端断开了链接...\n"); epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL); close(curfd); } else if(len>0) { printf("recv data: %s\n"); send(curfd, buf, len, 0); } else { perror("recv error"); exit(6); } } } }}
3.2 UDP3.2.1 服务器
- UDP服务器需要创建套接字
- 绑定端口
- 接收数据
- 根据接收数据的客户端发送数据
#include #include #include #include <string.h>#include int main(){ // 1.创建通信套接字 int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 2.接收数据需要绑定固定的端口 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8989); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret==-1) { perror("bind"); exit(0); } // 通信 char ip[24]; char buf[1024]; struct sockaddr_in cliaddr; int clilen = sizeof(cliaddr); while(1) { // 3.接收数据 int len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&cliaddr, &clilen); // 把发送端数据保存在cliaddr中 if(len==-1) { break; } printf("client ip: %s, port: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(cliaddr.sin_port)); // 打印发送端ip和port printf("client say: %s\n", buf); // 打印发送端发送的内容 // 4.回复数据 sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, clilen); } close(fd); return 0;}
3.2.2 客户端
- UDP客户端相对于服务器端减少了手动绑定ip端口的步骤
#include #include #include #include <string.h>#include int main(){ // 1.创建通信套接字 int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 服务器地址 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(8989); inet_pton(AF_INET, "10.0.2.15", &serveraddr.sin_addr.s_addr); // 通信 char ip[24]; char buf[1024]; int num=0; while(1) { // 2.发送数据 sprintf(buf, "Hello World!, %d\n", num++); sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); // 3.接收数据 memset(buf, 0, sizeof(buf)); int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); // 把发送端数据保存在cliaddr中 if(len==-1) { break; } printf("client say: %s\n", buf); // 打印发送端发送的内容 } close(fd); return 0;}
3.3 UDP广播3.3.1 服务器
- 服务器创建socket
- 设置广播属性
- 广播开销很小,只使用广播地址就可以发送到多个接收端
- 但只能在局域网内使用
- 发送端要设置广播属性,将消息发送到广播地址和端口,接收端在对应的端口等待
- 向广播ip端发送数据
#include #include #include #include <string.h>#include // 服务器就是广播端, 不需要收数据, 自动绑定了以后发数据就行int main(){ // 1.创建socket int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 2.设置广播属性 int opt = 1; // 1表示允许广播, 0表示不允许广播 setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt)); // 初始化数据接收端地址信息 struct sockaddr_in cliaddr; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(8989); inet_pton(AF_INET, "10.0.2.255", &cliaddr.sin_addr.s_addr); // 3.广播发送数据 char buf[1024]; int num = 0; while(1) { sprintf(buf, "发送广播数据: %d\n", num++); sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr)); printf("%s\n", buf); sleep(1); } close(fd); return 0;}
3.3.2 客户端
- 客户端创建socket
- 绑定固定的端口用来接收数据
- recvfrom接收数据
#include #include #include #include <string.h>#include int main(){ // 1.创建通信套接字 int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 绑定固定的端口 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8989); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret==-1) { perror("bind"); exit(0); } // 通信 char ip[24]; char buf[1024]; while(1) { // 接收数据 memset(buf, 0, sizeof(buf)); int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); // 把发送端数据保存在cliaddr中 if(len==-1) { break; } printf("boardcast say: %s\n", buf); // 打印发送端发送的内容 } close(fd); return 0;}
3.4 UDP组播
- 组播只需要发送到特定地址,发送端开销很小
- 组播需要组播地址,一种是Internet中使用,另一种是局域网使用,需要加入到多播组
- 相对于广播,组播支持广域网
3.4.1 服务器
#include #include #include #include <string.h>#include int main(){ // 1.创建socket int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 设置组播属性 struct in_addr addr; inet_pton(AF_INET, "239.0.0.10", &addr.s_addr); setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &addr, sizeof(addr)); // 初始化数据接收端地址信息 struct sockaddr_in cliaddr; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(10000); inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr); // 广播发送数据 char buf[1024]; int num = 0; while(1) { sprintf(buf, "组播数据: %d\n", num++); sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr)); printf("%s\n", buf); sleep(1); } close(fd); return 0;}
3.4.2 客户端
#include #include #include #include <string.h>#include #include <net/if.h>int main(){ // 1.创建通信套接字 int fd = socket(AF_INET, SOCK_DGRAM, 0); if(fd==-1) { perror("socket"); exit(0); } // 绑定固定的端口 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(10000); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret==-1) { perror("bind"); exit(0); } // 加入到多播组 struct ip_mreqn op; op.imr_address.s_addr = INADDR_ANY; // 本机地址 inet_pton(AF_INET, "239.0.0.10", &op.imr_multiaddr.s_addr); op.imr_ifindex = if_nametoindex("ens33"); // 网卡名转到序号 setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof(op)); // 通信 char ip[24]; char buf[1024]; while(1) { // 接收数据 memset(buf, 0, sizeof(buf)); int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL); // 把发送端数据保存在cliaddr中 if(len==-1) { break; } printf("boardcast say: %s\n", buf); // 打印发送端发送的内容 } close(fd); return 0;}
3.5 本地套接字用于进程间通信3.5.1 服务器端
#include #include #include #include #include #include int main(){ // 1.创建socket int fd = socket(AF_LOCAL, SOCK_STREAM, 0); // 2.和本地套接字文件绑定 struct sockaddr_un addr; addr.sun_family = AF_LOCAL; strcpy(addr.sun_path, "./server.sock"); // 套接字文件存储的目录 int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); printf("HELLO\n"); // 3.设置监听 listen(fd, 128); // 4.等待接收连接 struct sockaddr_un cliaddr; int clilen = sizeof(cliaddr); int cfd = accept(fd, (struct sockaddr*)&cliaddr, &clilen); printf("客户端套接字文件路径和名字:%s\n", cliaddr.sun_path); // 5.通信 while(1) { char buf[1024]; memset(buf, 0, sizeof(buf)); int len = recv(cfd, buf, sizeof(buf), 0); if(len==0) { printf("客户端断开连接\n"); break; } else if(len>0) { printf("client say: %s\n", buf); send(cfd, buf, len, 0); } else { perror("recv"); break; } } close(fd); return 0;}
3.5.2客户端
#include #include #include #include #include #include int main(){ // 1.创建socket int fd = socket(AF_LOCAL, SOCK_STREAM, 0); // 2.和本地套接字文件绑定 struct sockaddr_un addr; addr.sun_family = AF_LOCAL; strcpy(addr.sun_path, "./client.sock"); // 套接字文件存储的目录 int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); // 3.链接服务器 struct sockaddr_un seraddr; seraddr.sun_family = AF_LOCAL; strcpy(seraddr.sun_path, "./server.sock"); // 套接字文件存储的目录 ret = connect(fd, (struct sockaddr*)&seraddr, sizeof(seraddr)); // 4.通信 int num = 0; while(1) { // 发送数据 char buf[1024]; sprintf(buf, "本地套接字通信, %d\n", num++); send(fd, buf, strlen(buf)+1, 0); // 接收数据 memset(buf, 0, sizeof(buf)); recv(fd, buf, sizeof(buf), 0); printf("server say: %s\n", buf); } close(fd); return 0;}