字节序
- 字节序分为大端字节序和小端字节序,通信双方需要统一字节序才能正常通信。
- 主机字节序-小端字节序
- 网络字节序-大端字节序
判断大端字节序和小端字节序
/*
字节序:字节在内存中存储的顺序
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序:数据的高位字节存储在内存的低位地址,低位字节存储在内存的高位地址
*/
//通过代码检测当前主机的字节序
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
int main(){
union{
short value;//2字节
char bytes[sizeof(short)];//char[2]
}test;
test.value = 0x0102;
if((test.bytes[0] == 1) && (test.bytes[1] == 2)){
printf("大端字节序\n");
}else if((test.bytes[0] == 2) && (test.bytes[1] == 1)){
printf("小端字节序\n");
}else{
printf("未知");
}
return 0;
}
- 字节序转换函数
/*
网络通信时,需要将主机字节序转换成网络字节序(大端),
另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 - 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 - 网络字节序
*/
#include <stdio.h>
#include<arpa/inet.h>
int main(){
//htons 转换端口
unsigned short a = 0x0102;
printf("a : %x\n", a);
unsigned short b = htons(a);
printf("b : %x\n", b);
//htonl 转换ip
char buf[4] = {192, 168, 1, 100};
int num = *(int *)buf;
int sum = htonl(num);
unsigned char *p = (char *)∑
printf("%d %d %d %d\n",*p, *(p+1), *(p+2), *(p+3));
//ntohl
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char * p1 = (unsigned char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
//ntohs
unsigned short c = 0x2211;
printf("c : %x\n", c);
unsigned short d = ntohs(c);
printf("d : %x\n", d);
return 0;
}
TCP通信
-
-
客户端为什么不使用bind?
客户端也能用bind,但是不建议使用,服务器为什么要bind呢?因为服务器总是被动方,需要在一个众所周知的端口上等待连接请求,而且作为服务器它的端口号应该是固定的。服务器bind一个端口就表示会在这个端口提供一些特殊的服务。
而客户端它是主动发起方,我们并不关心是客户端的哪个端口和服务器建立了连接。内核会自动为我们分配一个随机的不冲突的端口号,如果我们对客户端bind的话,反而有可能导致端口冲突。
实现TCP通信服务端
//实现TCP通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
//创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
//inet_pton(AF_INET, "192.168.142.130", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = 0; //0.0.0.0 表示任意地址
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//监听
ret = listen(lfd, 8);
if(ret == -1){
perror("listen");
exit(-1);
}
//接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1){
perror("accept");
exit(-1);
}
//输出客户端的信息
char clientip[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientip, sizeof(clientip));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", clientip, clientPort);
//通信
//获取客户端的数据
char recvBuf[1024] = {0};
while(1){
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == 0){
perror("read");
exit(-1);
}else if(num > 0){
printf("recv client data : %s\n", recvBuf);
}else if(num == 0){
//表示客户端断开连接
printf("client closed...");
break;
}
//给客户端发送数据
char *data = "hello,i am server";
write(cfd, data, strlen(data));
}
//关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
实现TCP通信客户端
//实现TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
//创建套接字
int fd = socket(AF_INET,SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}
//连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.142.130", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//通信
char recvBuf[1024] = {0};
while(1){
//给服务器发送数据
char *data = "hello,i am client";
write(fd, data, strlen(data));
sleep(1);
//获取服务器端的数据
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == 0){
perror("read");
exit(-1);
}else if(len > 0){
printf("recv server data : %s\n", recvBuf);
}else if(len == 0){
//表示服务器断开连接
printf("server closed...");
break;
}
}
close(fd);
return 0;
}
多进程实现TCP并发
- 服务端实现
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recyChild(int arg){
while(1){
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1){
//所有的子进程都回收了
break;
}else if(ret == 0){
//还有子进程活着
break;
}else if(ret > 0){
//被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main(){
//注册信号捕捉
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyChild;
sigaction(SIGCHLD, &act, NULL);
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//监听
ret = listen(lfd, 128);
if(ret == -1){
perror("listen");
exit(-1);
}
//不断循环等待客户端连接
while(1){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1){
//当信号捕捉时,会执行回调函数,造成短暂的软中断,程序报错
if(errno == EINTR){
continue;
}
perror("accept");
exit(-1);
}
//每一个连接进来,创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0){
//子进程
//获取客户端的信息
char cliIP[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, port is %d\n", cliIP, cliPort);
//接收客户端发来的数据
char recvbuf[1024] = {0};
while(1){
int len = read(cfd, &recvbuf, sizeof(recvbuf));
if(len == -1){
perror("read");
exit(-1);
}else if(len > 0){
printf("recv client data : %s\n", recvbuf);
}else if(len == 0){
printf("client closed\n");
break;
}
write(cfd, recvbuf, strlen(recvbuf));
}
close(cfd);
exit(0);//退出当前子进程
}
//通过信号释放子进程资源
}
close(lfd);
return 0;
}
- 客户端实现
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(){
//创建套接字
int fd = socket(AF_INET,SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}
//连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.142.130", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//通信
char recvBuf[1024] = {0};
int i = 0;
while(1){
sprintf(recvBuf, "data : %d\n", i++);
//给服务器发送数据
write(fd, recvBuf, strlen(recvBuf));
sleep(1);
//获取服务器端的数据
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == 0){
perror("read");
exit(-1);
}else if(len > 0){
printf("recv server data : %s\n", recvBuf);
}else if(len == 0){
//表示服务器断开连接
printf("server closed...");
break;
}
}
close(fd);
return 0;
}
多线程实现TCP并发
- 服务端实现
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
struct sockInfo{
int fd;//通信的文件描述符
pthread_t tid;//线程号
struct sockaddr_in addr;//客户端的信息
};
struct sockInfo sockinfos[128];
void *working(void * arg){
//子线程和客户端通信
//获取客户端的信息
struct sockInfo * pinfo = (struct sockInfo *)arg;
char cliIP[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIP, sizeof(cliIP));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, port is %d\n", cliIP, cliPort);
//接收客户端发来的数据
char recvbuf[1024] = {0};
while(1){
int len = read(pinfo->fd, &recvbuf, sizeof(recvbuf));
if(len == -1){
perror("read");
exit(-1);
}else if(len > 0){
printf("recv client data : %s\n", recvbuf);
}else if(len == 0){
printf("client closed\n");
break;
}
write(pinfo->fd, recvbuf, strlen(recvbuf));
}
close(pinfo->fd);
return NULL;
}
int main(){
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//监听
ret = listen(lfd, 128);
if(ret == -1){
perror("listen");
exit(-1);
}
//初始化数据
int max = sizeof(sockinfos)/sizeof(sockinfos[0]);
for(int i=0; i<max; i++){
bzero(&sockinfos[i],sizeof(sockinfos[i]));
sockinfos[i].fd = -1;//初始化为-1表示可用
sockinfos[i].tid = -1;
}
//循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
//接收连接
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
struct sockInfo *pinfo;
for(int i=0; i<max; i++){
//从数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1){
pinfo = &sockinfos[i];
break;
}
//如果达到数组最大值,等待一秒
if(i == max - 1){
sleep(1);
i--;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);
pthread_t tid;
//创建子线程
pthread_create(&pinfo->tid, NULL, working, pinfo);
//线程分离,不需要父线程回收资源
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
端口复用
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
- 端口复用技术setsockopt()
I/O多路复用(I/O多路转换)
-
I/O多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux下实现I/O多路复用的系统调用主要有select、poll和epoll
-
多路复用的衍生:多进程/多线程->slect->poll->epoll
-
多路复用将多个文件描述符存入到一个集合中,提供了中间站检测文件描述符中的读写缓冲区是否有数据;多进程/多线程进行TCP通信时,需要多次循环遍历读写缓冲区查看有无数据,造成CPU资源的浪费。
-
select()工作过程分析
从用户态拷贝到内核态,只有A,B发送了数据,委托内核将A,B发送数据所对应的文件描述符置为1,没有发送数据的文件描述符置为0,其次返回给用户态中,开始读取数据。
select.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>
#include<sys/select.h>
int main(){
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
//绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
//监听
listen(lfd, 8);
//创建一个fd_set集合,存放的是需要检测的文件描述符
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1){
tmp = rdset;
//调用select系统函数,让内核帮检测那些文件描述符有数据
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1){
perror("select");
exit(-1);
}else if(ret == 0){
continue;
}else if(ret > 0){
//说明检测到文件描述符对应的缓冲区的数据发生了改变
if(FD_ISSET(lfd, &tmp)){
//表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
//将新的文件描述符添加到集合中
FD_SET(cfd, &rdset);
//更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
for(int i = lfd + 1; i <= maxfd; i++){
if(FD_ISSET(i, &tmp)){
//说明文件描述符发来了数据
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
close(i);
FD_CLR(i, &rdset);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
- select的缺点
- select默认的文件描述符是1024比特位,共有128字节,代表1024个文件描述符。
- fds集合不能重用需要多创建一个临时集合供内核进行修改,不会对原始的集合做出改变,避免影响下次调用。
- poll
- 不同于select的是定义了结构体存放文件描述符和检测的事件,由内核返回发生的事件,不会修改原始的事件,此外,没有大小的限制。但是也具有select的缺点,只是稍微地改进了一下。
- poll的缺点:同select一样,每次的集合都需要从用户态拷贝到内核态,由内核态检测再返回到用户态中,中间过程消耗时间。
poll.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>
#include <poll.h>
int main(){
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
//绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
//监听
listen(lfd, 8);
//初始化检测的文件描述符数组
struct pollfd fds[1024];
for(int i = 0; i < 1024; i++){
fds[i].fd = -1;
fds[i].events = POLLIN;//检测读事件
}
//将监听的文件描述符添加到数组中
fds[0].fd = lfd;
int nfds = 0;
while(1){
//调用poll系统函数,让内核帮检测那些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if(ret == -1){
perror("poll");
exit(-1);
}else if(ret == 0){
continue;
}else if(ret > 0){
//说明检测到文件描述符对应的缓冲区的数据发生了改变
if(fds[0].revents & POLLIN){
//表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
//将新的文件描述符添加到集合中
for(int i = 1; i < 1024; i++){
if(fds[i].fd == -1){
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
//更新最大的文件描述符的索引
nfds = nfds > cfd ? nfds : cfd;
}
for(int i = 1; i <= nfds; i++){
if(fds[i].revents & POLLIN){
//说明文件描述符发来了数据
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
close(fds[i].fd);
fds[i].fd = -1;
}else if(len > 0){
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
- epoll
- 不用拷贝集合到内核中,在内核态中创建数据结构eventpoll,提高了遍历的效率。
- 调用epoll_wait函数检测rbr数据结构中的文件描述符是否有数据传过来,如果有几个文件描述符发生改变就将其拷贝到rdlist,(只拷贝一次并且只拷贝有数据的文件描述符),然后返回到用户态中。
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>
#include <sys/epoll.h>
int main(){
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
//绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
//监听
listen(lfd, 8);
//调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
//将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1){
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1){
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++){
int curfd = epevs[i].data.fd;
if(curfd == lfd){
//监听的文件描述符有数据到达,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN | EPOLLOUT;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}else{
if(epevs[i].events & EPOLLOUT){
continue;
}
//有数据到达,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
-
epoll的两种工作模式
-
LT模式
-
将服务端的读取的缓冲区减小,在客户端输入的字节数大于缓冲区,说明服务端需要多次读取。这就是水平触发,每次调用都会epoll_wait()都会检测通知。
-
服务端
-
客户端
-
ET模式
-
EPOLLET(边沿触发)
-
设置边沿触发
-
服务端
-
每次读取缓冲区大小的数据,阻塞在epoll_wait()
-
客户端
-
ET模式设置循环读取数据
-
将read()通过fcntl()设置为非阻塞,在while循环中,注意的是当缓冲区的数据读取完之后,继续读取会出现EAGAIN的错误。
-
服务端
-
客户端
UDP通信
- 服务端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main(){
//创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
//绑定
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1){
perror("bind");
exit(-1);
}
//通信
while(1){
char recvbuf[1024];
char ipbuf[16];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
//接收数据
int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);
if(num == -1){
perror("recvform");
exit(-1);
}
printf("client IP : %s, Port : %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(cliaddr.sin_port));
printf("client say : %s\n", recvbuf);
//发送数据
sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
}
close(fd);
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main(){
//创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}
//服务器的地址信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);
int num = 0;
//通信
while(1){
char sendBuf[128];
sprintf(sendBuf, "hello, i am client %d\n", num++);
//发送数据
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));
//接收数据
int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
if(num == -1){
perror("recvform");
exit(-1);
}
printf("client say : %s\n", sendBuf);
sleep(1);
}
close(fd);
}