Linux网络编程基础5(epoll,epoll三种工作模式,UDP通信流程,TCP和UDP的使用场景)

Linux网络编程基础5(epoll,epoll三种工作模式,UDP通信流程)

  • 1. epoll
    • 1.1 相关函数
      • 1.1.1 epoll_create
      • 1.1.2 epoll_ctl
      • 1.1.3 epoll_wait
    • 1.2 epoll比select和poll效率高的原因
    • 1.3 epoll注意事项
    • 1.4 epoll工作流程伪代码
    • 1.5 epoll应用实例完整代码
    • 1.6 epoll应用实例2
  • 2. epoll的三种工作模式
    • 2.1 水平触发模式(默认工作模式)
    • 2.2 边沿触发模式——ET(是阻塞的)
    • 2.3 边沿非阻塞触发——效率最高
  • 3. UDP通信
    • 3.1 UDP通信相关函数
    • 3.2 UDP通信流程
    • 3.3 UDP服务端实现代码
    • 3.4 UDP客户端实现代码
  • 4. TCP和UDP的使用场景

1. epoll

  epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

1.1 相关函数

1.1.1 epoll_create

生成一个epoll专用的文件描述符

int epoll_create(int size);  参数:
size: epoll上能关注的最大描述符数。
(真正用的时候随便传一个就行,epoll在容量不够的时候会做自动扩展,类似vector的size并不是永久不变的)

  epoll在内部是一个红黑树结构,文件描述符默认0-2被终端占用。每次有一个新的文件描述符,就创建一个树节点。epoll默认设置树上最多的节点数是2000,但是如果数量超过2000,则大小会被自动扩展。

在这里插入图片描述

1.1.2 epoll_ctl

用于控制某个epoll文件描述符事件,可以注册、修改、删除

int epoll_ctl(	int epfd,  //epoll_create的返回值int op,      //指定对应操作的宏int fd,      //下侧结构体epoll_data里面的fdstruct epoll_event *event); 参数:epfd: 	epoll_create生成的epoll专用描述符op: EPOLL_CTL_ADD    --   注册   EPOLL_CTL_MOD    --   修改  EPOLL_CTL_DEL    --   删除fd: 	关联的文件描述符event: 	告诉内核要监听什么事件

epoll_ctl的第四个参数struct epoll_event *event的结构体原型:

typedef union epoll_data {void        *ptr;int          fd;uint32_t     u32;uint64_t     u64;
} epoll_data_t;struct epoll_event {uint32_t     	events;                          epoll_data_t   data; 
};events:- EPOLLIN 	-- EPOLLOUT 	-- EPOLLERR 	- 	异常

eopll树上每一个节点挂的都是一个结构体,这个结构体就是epoll_event,就是epoll_ctl的第四个参数。

epoll_event可以存储对应文件描述符的描述,而且描述的内容不限制,我们可以自定义一些需要存储的属性在一个我们自己写的一个结构体里面,让epoll_data里面的ptr指针指向这个结构体里面的地址。因此epoll里面存储数据很灵活。


1.1.3 epoll_wait

等待IO事件发生 - 可以设置阻塞的函数,类比select和poll函数

int epoll_wait(int epfd,                     //epoll_create的返回值struct epoll_event* events,  // 结构体指针,发生改变的文件描述符元素是存在epoll_event结构体里,//当发生改变,内核会把改变了的epoll_event拷贝到epoll_wait函数的第二个参数里面int maxevents,               //数组的容量int timeout                  //函数是否阻塞
);参数:epfd: 		要检测的句柄,epoll_create的返回值events:	用于回传待处理事件的数组,他是一个传出参数,他里面的值是内核拷贝过来的,发生了改变的,装有文件描述符的epoll_event结构体maxevents:	告诉内核这个events的大小timeout:	为阻塞时间-1: 永久阻塞0: 立即返回>0:阻塞时间

epoll_wait函数相当于我们之前见过的select函数或者poll函数,我们在变成的时候要在对应的位置写的就是epoll_wait函数。
如果epoll_ctlop是一个add或者modified,那么第四个参数epoll_event是一个传入参数
epoll_wait的时候,epoll_event就是一个传出参数了。作用不一样。一个是往树上挂,一个是从树上拷贝,拷贝到结构体里面。


1.2 epoll比select和poll效率高的原因

  • select内部使用数组实现,poll是链表。他们需要做内核区到用户区的转换,还需要做数据拷贝,因此效率低。
  • epoll不需要做内核区到用户区的转换,因为数据存在共享内存中。epoll维护的树在共享内存中,内核区和用户区去操作共享内存,因此不需要区域转换,也不需要拷贝操作。

1.3 epoll注意事项

  • 共享内存中,epoll维护了一个树状结构(红黑树)。红黑树的根节点通过调用epoll_create()就创建了一个根(根节点中不存放待检测的文件描述符)。执行epoll时返回的整数返回值,这个整数可以看成是文件描述符。(epoll_create的参数如果你不清楚,那么系统会自动给你扩展,但是扩展是有上限的。如果你的电脑是1G内存,则他的上限是10万,即1G内存可以在epoll树上挂10万个节点)
  • 在根节点上添加节点,就需要调用eopll_ctl()函数
  • 在检测的时候,对eopll树进行遍历。检测文件描述符是否发生变化,如果发生变化,则epoll_wait进行返回,如果没有变化,则epoll_wait就在那阻塞着

在这里插入图片描述


1.4 epoll工作流程伪代码

int main(){// 创建监听的套接字int lfd = socket();// 绑定bind();// 监听listen();// epoll树根节点,传入的参数不用纠结,epoll会自动扩展int epfd = epoll_create(3000);// 存储发送变化的fd对应信息struct epoll_event all[3000];// 将用于监听的lfd挂到epoll树上struct epoll_event ev;// 在ev中init lfd信息ev.events = EPOLLIN ;ev.data.fd = lfd;//epfds epoll_create的返回值//EPOLL_CTL_ADD添加节点//lfd文件描述符//ev是结构体指针,结构体需要初始化一下epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);while(1){// 委托内核检测事件//epfd是epoll_create创建出来的//all是一个传出参数,用于存储发生变化的fd对应信息//-1指的是永久阻塞//有多少个发生了变化int ret = epoll_wait(epfd, all, 3000, -1);// 根据ret遍历all数组//发生变化的都会存在all数组中//变化的情况有两种for(int i=0; i<ret; ++i){int fd = all[i].data.fd;// 有新的连接if( fd == lfd){// 接收连接请求 - accept不阻塞int cfd = accept();// cfd上树ev.events = EPOLLIN;ev.data.fd = cfd;epoll_ctl(epfd, epoll_ctl_add, cfd, &ev);}// 已经连接的客户端有数据发送过来else{// 只处理客户端发来的数据if(!all[i].events & EPOLLIN){continue;}// 读数据int len = recv();//客户端关闭了链接if(len == 0){close(fd);// 检测的fd从树上删除epoll_ctl(epfd, epoll_ctl_del, fd, NULL);}// 写数据send();}}}
}

1.5 epoll应用实例完整代码

#include
#include
#include
#include
#include
#include
#include
#include
#includeint main(int argc, char *argv[]){if(argc<2){printf("eg: ./a.out port \n");exit(1);}struct sockaddr_in serv_addr;socklen_t serv_len=sizeof(serv_addr);//字符串转整型的函数atoiint port=atoi(argv[1]);//创建套接字,tcp字节流协议,所以用SOCK_STREAMint lfd=socket(AF_INET, SOCK_STREAM, 0);//初始化服务器 memset(&serv_addr, 0, serv_len);//地址族serv_addr.sin_family=AF_INET;//监听本地所有IP。htonlserv_addr.sin_addr.s_addr=htonl(INADDR_ANY);//设置端口,从主机字节序转网络字节序,htons小端转大端serv_addr.sin_port=htons(port);//绑定IP和端口bind(lfd, (struct sockaddr*)&serv_addr, serv_len);//设置同时监听的最大个数listen(lfd, 36);printf("Start accept .... \n");struct sockaddr_in client_addr;socklen_t cli_len=sizeof(client_addr);//创建epoll树的根节点int epfd=epoll_create(2000);//初始化epoll树struct epoll_event ev;//读入,所以是EPOLLINev.events=EPOLLIN;ev.data.fd=lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);struct epoll_event all[2000];while(1){//使用epoll通知内核,帮我们去做文件流检测int ret=epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);//遍历数组的前ret个元素for(int i=0;i<ret; i++){int fd=all[i].data.fd;//判断是否是新连接if(fd==lfd){int cfd=accept(lfd, (struct sockaddr*)&client_addr, &cli_len);if(cfd==-1){perror("accept error");exit(1);}//将新得到的cfd挂在树上struct epoll_event temp;temp.events=EPOLLIN;temp.data.fd=cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);//打印新连接到的信息char ip[64]={0};printf("New Client IP:%s, Port: %d\n",inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),ntohs(client_addr.sin_port));}//已连接的客户端发来了数据else{//如果能进入该条件里,说明EPOLLIN不在这个events里面if(!all[i].events&EPOLLIN){continue;}//读数据char buf[1024]={0};int len=recv(fd, buf, sizeof(buf), 0);if(len==-1){perror("recv error");exit(1);}else if(len==0){printf("client disconnected ...\n");epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);if(ret==-1){perror("epoll_ctl - del error");exit(1);}close(fd);}else{printf("recv buf: %s\n", buf);write(fd, buf, len);}}}}close(lfd);return 0;
}

1.6 epoll应用实例2

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include typedef struct sockinfo{int fd;//可以存ip和端口struct sockaddr_in sock;
}SockInfo;int main(int argc, const char* argv[]){if(argc < 2){printf("./a.out port\n");exit(1);}int lfd, cfd;struct sockaddr_in serv_addr, clien_addr;int serv_len, clien_len;int port = atoi(argv[1]);// 创建套接字lfd = socket(AF_INET, SOCK_STREAM, 0);// 初始化服务器 sockaddr_in memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;                 // 地址族 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听本机所有的IPserv_addr.sin_port = htons(port);               // 设置端口 serv_len = sizeof(serv_addr);// 绑定IP和端口bind(lfd, (struct sockaddr*)&serv_addr, serv_len);// 设置同时监听的最大个数listen(lfd, 36);printf("Start accept ......\n");// 创建红黑树根节点int epfd = epoll_create(2000);if(epfd == -1){perror("epoll_create error");exit(1);}// lfd添加到监听列表 SockInfo* sinfo = (SockInfo*)malloc(sizeof(SockInfo));sinfo->sock = serv_addr;//lfd绑定的是服务器的ip和端口sinfo->fd = lfd;struct epoll_event ev;//ptr指针指向sinfo那快内存。//为了保证ptr指向的指针地址是有效的,所以sinfo申请的是堆内存,自己不释放,地址就还有效ev.data.ptr = sinfo;ev.events = EPOLLIN;//将用于监听的lfd文件描述符挂在了树上int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);if(ret == -1) {perror("epoll_ctl error");exit(1);}struct epoll_event res[2000];while(1) {            // 设置监听//文件描述符的变动信息会写在传出参数res这个数组中ret = epoll_wait(epfd, res, sizeof(res)/sizeof(res[0]), -1);if(ret == -1) {perror("epoll_wait error");exit(1);}// 遍历前ret个元素for(int i=0; i<ret; ++i){int fd = ((SockInfo*)res[i].data.ptr)->fd;if(res[i].events != EPOLLIN){continue;}//接受连接请求// 判断文件描述符是否为lfd//这里我们定义了一个变量,如果有多个客户端,其实每个客户端都需要这样的一块地址if(fd == lfd){char ipbuf[64];//每建立一个链接,我们就开辟一块内存空间来存储客户端对应的信息SockInfo *info = (SockInfo*)malloc(sizeof(SockInfo));clien_len = sizeof(clien_addr);cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);// cfd 添加到监听树info->fd = cfd;info->sock = clien_addr;struct epoll_event ev;ev.events = EPOLLIN;ev.data.ptr = (void*)info;//将新的连接添加到树上epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);printf("client connected, fd = %d, IP = %s, Port = %d\n", cfd, inet_ntop(AF_INET, &clien_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)), ntohs(clien_addr.sin_port));}else {// 通信char ipbuf[64];char buf[1024] = {0};int len = recv(fd, buf, sizeof(buf), 0);//创建指针,让指针指向res数组元素中的ptr//如此我们可以获取客户端的信息SockInfo* p = (SockInfo*)res[i].data.ptr;if(len == -1){perror("recv error");exit(1);}//此时客户端发起断开连接请求else if(len == 0) {// 取客户端的ip//p->sock.sin_port 可以获取端口inet_ntop(AF_INET, &p->sock.sin_addr.s_addr, ipbuf, sizeof(ipbuf));printf("client %d 已经断开连接, Ip = %s, Port = %d\n", fd, ipbuf, ntohs(p->sock.sin_port));// 节点从树上删除epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);close(fd);free(p);}else{printf("Recv data from client %d, Ip = %s, Port = %d\n", fd, ipbuf, ntohs(p->sock.sin_port));printf("    === buf = %s\n", buf);send(fd, buf, strlen(buf)+1, 0);}}}}close(lfd);free(sinfo);return 0;
}

2. epoll的三种工作模式

  epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。


2.1 水平触发模式(默认工作模式)

  只要fd对应的缓冲区有数据,epoll_wait就会返回。返回的次数与发送数据的次数无关,只与你read缓冲区中是否有数据有关(如果有的时候你没读完,由于read里面还有剩余,因此会触发epoll_wait函数再次被调用)

有一个新连接,那么epoll_wait就会返回一次



对于下面代码,使用printf会产生一个bug

                        //已连接的客户端发来了数据else{//如果能进入该条件里,说明EPOLLIN不在这个events里面if(!all[i].events&EPOLLIN){continue;}//读数据char buf[5]={0};int len=recv(fd, buf, sizeof(buf), 0);if(len==-1){perror("recv error");exit(1);}else if(len==0){printf("client disconnected ...\n");epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);if(ret==-1){perror("epoll_ctl - del error");exit(1);}close(fd);}else{printf("recv buf: %s\n", buf);write(fd, buf, len);}}

  C标准库函数都有一个缓冲区,缓冲区大小默认的8k,printf就是C库函数。不写\n,则不写满缓冲区它就不打印。
  当客户端发来一段很长的数据的时候,由于我们定义的buf里面没有\0,因此printf在读buf的时候会一直往后读去找\0,因此会printf一些乱码。因此我们不用printf了,换成write

write(STDOUT_FILENO,buf,len);

2.2 边沿触发模式——ET(是阻塞的)

  客户端给server发送数据,每发一次数据,server的epoll_wait就会被触发一次、不在乎数据是否读完

  边沿模式为了提高epoll的效率,因为epoll_wait每调用一次都有系统开销,使用边沿模式可以减少epoll_wait的调用次数。

  在代码中想使用边沿触发模式的方法——只要在水平模式的基础上,将epoll_event结构体中的events事件中按位或一个宏(EPOLLET)即可

//将新得到的cfd挂在树上
struct epoll_event temp;
//EPOLLIN是水平触发,希望使用边缘模式,则加一个EPOLLET
// | 是按位或
temp.events=EPOLLIN | EPOLLET;
temp.data.fd=cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);

2.3 边沿非阻塞触发——效率最高

如何设置非阻塞

  1. open() 函数里面有个flags,通过设置flags为 O_WDRW | O_NONBLOCK,则打开之后的文件描述符就被设置成非阻塞的了。这个适用于终端( 例如/dev/tty当前操作的是哪一个终端,则tty就对应哪一个终端 )
  2. fcntl 可以实现的功能有五种(复制文件描述符、修改文件描述符对应的flags属性…)

fcntl修改文件描述符对应的flags属性为非阻塞的例子

//设置文件cfd为非阻塞模式
//使用fcntl函数需要加头文件fcntl.h
int flag=fcntl(cfd, F_GETFL);
flag |=O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);

将缓冲区的全部数据都读出

对于边沿触发模式,如果我们想把read缓冲区里面的数据都读出来,可以试试如下方式:

while(recv()>0){printf();
}

当缓冲区数据读完之后,recv的返回值是否为0?

答案——返回值是-1

因为——读完之后,recv强行读了一个没有数据的缓冲区,因此返回了-1.

如何解决?

判断errno是否等于EAGAIN,如果是,则说明造成recv返回值为-1的原因是强行读了没有数据的缓冲区。

//读数据
char buf[5]={0};
int len;
//循环读数据
while((len=recv(fd, buf, sizeof(buf), 0))>0){//数据打印到中断write(STDOUT_FILENO, buf, len);//发送给客户端send(fd, buf, len, 0);
}
if(len==-1){if(errno==EAGAIN){printf("缓冲区读完\n");}else{perror("recv error");exit(1);}
}

边沿非阻塞触发完整代码如下:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#includeint main(int argc, char *argv[]){if(argc<2){printf("eg: ./a.out port \n");exit(1);}struct sockaddr_in serv_addr;socklen_t serv_len=sizeof(serv_addr);//字符串转整型的函数atoiint port=atoi(argv[1]);//创建套接字,tcp字节流协议,所以用SOCK_STREAMint lfd=socket(AF_INET, SOCK_STREAM, 0);//初始化服务器 memset(&serv_addr, 0, serv_len);//地址族serv_addr.sin_family=AF_INET;//监听本地所有IP。htonlserv_addr.sin_addr.s_addr=htonl(INADDR_ANY);//设置端口,从主机字节序转网络字节序,htons小端转大端serv_addr.sin_port=htons(port);//绑定IP和端口bind(lfd, (struct sockaddr*)&serv_addr, serv_len);//设置同时监听的最大个数listen(lfd, 36);printf("Start accept .... \n");struct sockaddr_in client_addr;socklen_t cli_len=sizeof(client_addr);//创建epoll树的根节点int epfd=epoll_create(2000);//初始化epoll树struct epoll_event ev;//读入,所以是EPOLLINev.events=EPOLLIN;ev.data.fd=lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);struct epoll_event all[2000];while(1){//使用epoll通知内核,帮我们去做文件流检测int ret=epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);//遍历数组的前ret个元素for(int i=0;i<ret; i++){int fd=all[i].data.fd;//判断是否是新连接if(fd==lfd){int cfd=accept(lfd, (struct sockaddr*)&client_addr, &cli_len);if(cfd==-1){perror("accept error");exit(1);}//设置文件cfd为非阻塞模式//使用fcntl函数需要加头文件fcntl.hint flag=fcntl(cfd, F_GETFL);flag |=O_NONBLOCK;fcntl(cfd, F_SETFL, flag);//将新得到的cfd挂在树上struct epoll_event temp;//EPOLLIN是水平触发,希望使用边缘模式,则加一个EPOLLETtemp.events=EPOLLIN;temp.data.fd=cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);//打印新连接到的信息char ip[64]={0};printf("New Client IP:%s, Port: %d\n",inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),ntohs(client_addr.sin_port));}//已连接的客户端发来了数据else{//如果能进入该条件里,说明EPOLLIN不在这个events里面if(!all[i].events&EPOLLIN){continue;}//读数据char buf[5]={0};int len;//循环读数据while((len=recv(fd, buf, sizeof(buf), 0))>0){//数据打印到中断write(STDOUT_FILENO, buf, len);//发送给客户端send(fd, buf, len, 0);}if(len==-1){if(errno==EAGAIN){printf("缓冲区读完\n");}else{perror("recv error");exit(1);}}else if(len==0){printf("client disconnected ...\n");epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);if(ret==-1){perror("epoll_ctl - del error");exit(1);}close(fd);}else{//printf会产生乱码,因为buf里面没有\0//printf("recv buf: %s\n", buf);write(STDOUT_FILEND, buf, len);write(fd, buf, len);}}}}close(lfd);return 0;
}

3. UDP通信

UDP通信有个特点是不管是服务端还是客户端,它对应的文件描述符只有一个,而且创建套接字之后直接通信即可,不用connect。

tcp - 面向连接的安全的数据包通信

  • 基于流 sock_stream

udp - 面向无连接不安全报文传输


3.1 UDP通信相关函数

服务器端
创建套接字 - socket

  • 第二个参数: SOCK_DGRAM

绑定IP和端口: bind

  • struct sockaddr – 服务器
  • fd

通信

  • 接收数据: recvfrom
ssize_t recvfrom(	int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数:sockfd - 创建套接字返回的文件描述符buf: 接收数据缓冲区len: buf的最大容量flags: 0src_addr: 另一端的IP和端口, 传出参数addrlen: 传入传出参数
  • 发送数据: sendto
ssize_t sendto(	int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数:sockfd: socket函数创建出来的buf: 存储发送的数据len: 发送的数据的长度flags: 0dest_addr: 另一端的IP和端口addrlen: dest_addr长度

客户端
创建一个用于通信的套接字: socket

通信

  • 发送数据: sendto,需要先准备好一个结构体: struct sockaddr_in,存储服务器的IP和端口
  • 接收数据: recvform

udp的数据是不安全的, 容易丢包

丢包, 丢全部还一部分?

只能丢全部

优点: 效率高

3.2 UDP通信流程

在这里插入图片描述

3.3 UDP服务端实现代码

#include
#include
#include
#include
#include
#include
#includeint main(int argc, const char* argv[]){//创建套接字//tcp,ued通信,则第一个参数是AF_INET//第三个参数是协议,这里传0int fd=socket(AF_INET, SOCK_DGRAM, 0);if(fd==-1){perror("socket error");exit(1);}//fd绑定本地IP和端口struct sockaddr_in serv;memset(&serv, 0, sizeof(serv));serv.sin_family=AF_INET;serv.sin_port=htons(8765);serv.sin_addr.s_addr=htonl(INADDR_ANY);int ret=bind(fd, (struct sockaddr*)&serv, sizeof(serv));if(ret==-1){perror("bind error");exit(1);}struct sockaddr_in client;socklen_t cli_len=sizeof(client);char buf[1024]={0};//通信while(1){//第一个参数——socket函数创建的套接字//第二个参数——接收数据的缓冲区//第三个参数——buf的最大容量//第四个参数——一个传出参数//只要你一接收数据,就会保存对方的ip地址和端口号//客户端的ip地址和端口存在第五个采纳数里面int recvlen=recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&client, &cli_len);printf("recv buf:%s\n", buf);char ip[64]={0};printf("new client ip:%s, port:%d\n",inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)), ntohs(client.sin_port));//给客户端发送数据//把接受的数据再传回给客户端sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&client, sizeof(client));}close(fd);return 0;
}

3.4 UDP客户端实现代码

#include
#include
#include
#include
#include
#include
#includeint main(int argc, const char* argv[]){//创建套接字int fd=socket(AF_INET, SOCK_DGRAM, 0);if(fd==-1){perror("socket error");exit(1);}//初始化服务器的ip和端口struct sockaddr_in serv;memset(&serv, 0, sizeof(serv));serv.sin_family=AF_INET;serv.sin_port=htons(8765);//inet_pton 把点分十进制的ip改成整型ipinet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);//通信while(1){char buf[1024]={0};//从终端收到数据,然后把数据发出去fgets(buf, sizeof(buf), stdin);//数据发送 //第三个参数是发送数据的实际大小,不是buf的大小sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&serv, sizeof(serv));//等待服务器发送数据过来//udp只能使用sendto和recvfromrecvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);printf("recv buf:%s\n", buf);}close(fd);return 0;
}

4. TCP和UDP的使用场景

tcp使用场景

  • 对数据安全性要求高的时候

    登录数据的传输
    文件传输

  • http协议的底层传输使用的时候

    传输层协议 - tcp

udp使用场景

  • 效率高 - 实时性要求比较高
    视频聊天
    通话
    屏幕共享软件

  • 有实力的大公司在做通讯工具时
    使用upd,在应用层自定义协议, 做数据校验


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部