linux 网络编程 2019.1.13 (epoll,epoll的三种触发方式,UDP通信,tcp和udp的使用场景)

学习目标

  1. 了解poll操作函数
  2. 熟练使用epoll多路IO模型
  3. 了解epoll ET/LT触发模式
  4. 说出UDP的通信流程

 

 

一些零散的操作

 

gdb设置参数

 

vim中查找某个词

 

段错误出现的原因

  1. 你访问了不改访问的内存
  2. 你访问的内存,你没权限
  3. 你操作的内存根本就不存在

 

 

 

epoll

 

三个函数

 

1、该函数生成一个epoll专用的文件描述符

int epoll_creae(int size);  

参数:

size: epoll上能关注的最大描述符数。(真正用的时候随便穿一个就行,epoll在容量不够的时候会做自动扩展)

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

 

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

int epoll_ctl(int epfd,  //epoll_create的返回值
            int op,      //指定对应操作的宏
            int fd,      //下侧结构体epoll_data里面的fd
            struct epoll_event *event); 

参数:

 

epoll_ctl的第四个参数的结构体原型:

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里面存储数据很灵活。

 

 

3、等待IO事件发生 - 可以设置阻塞的函数

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_ctl的op是一个add或者modified,那么第四个参数epoll_event是一个传入参数   
  • epoll_wait的时候,epoll_event就是一个传出参数了。作用不一样。一个是往树上挂,一个是从树上拷贝,拷贝到结构体里面。

 

 

epoll比select和poll效率高的原因是:

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

 

epoll复习

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

 

 

 

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();
			}
		}
	}
}

 

 

epoll应用实例

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/epoll.h>

int 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);
	//字符串转整型的函数atoi
	int port=atoi(argv[1]);
	
	//创建套接字,tcp字节流协议,所以用SOCK_STREAM
	int lfd=socket(AF_INET, SOCK_STREAM, 0);
	//初始化服务器 
	memset(&serv_addr, 0, serv_len);
	//地址族
	serv_addr.sin_family=AF_INET;
	//监听本地所有IP。htonl
	serv_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;
	//读入,所以是EPOLLIN
	ev.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;
}

 

 

epoll应用实例2

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>

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);  // 监听本机所有的IP
    serv_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;
}

 

 

 

epoll的三种工作模式

 

水平触发模式(默认工作模式)——根据读来解释

只要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

 

 

边沿触发模式——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);

 

 

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

如何设置非阻塞

  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<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<error.h>

int 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);
	//字符串转整型的函数atoi
	int port=atoi(argv[1]);
	
	//创建套接字,tcp字节流协议,所以用SOCK_STREAM
	int lfd=socket(AF_INET, SOCK_STREAM, 0);
	//初始化服务器 
	memset(&serv_addr, 0, serv_len);
	//地址族
	serv_addr.sin_family=AF_INET;
	//监听本地所有IP。htonl
	serv_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;
	//读入,所以是EPOLLIN
	ev.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.h
				int flag=fcntl(cfd, F_GETFL);
				flag |=O_NONBLOCK;
				fcntl(cfd, F_SETFL, flag);

				//将新得到的cfd挂在树上
				struct epoll_event temp;
				//EPOLLIN是水平触发,希望使用边缘模式,则加一个EPOLLET
				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[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;
}

 

 

 

 

文件描述符突破1024限制

select突破不了,需要编译内核

  • 通过数组实现

poll和epoll可以突破1024限制

  • poll内部——链表
  • epoll——红黑树

 

1、查看收计算机硬件限制的文件描述符上限的命令

cat /proc/sys/fs/file-max

2、通过配置文件修改上限

/etc/security/limits.conf

打开该文件,将如下两行文字加入到文件中

然后重启虚拟机即可修改文件描述符上限。

3、通过命令行进行修改

ulimit -a

执行命令

ulimit -n 8000

 

 

 

 

UDP通信——不管是服务端还是客户端,他对应的文件描述符只有一个

创建套接字之后直接通信即可,不用connect。

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

  • 基于流  sock_stream

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);
  • fd - 文件描述符
  • buf: 接收数据缓冲区
  • len: buf的最大容量
  • flags: 0
  • src_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: 0
  • dest_addr: 另一端的IP和端口
  • addrlen: dest_addr长度

udp服务器端: 需要一个套接字, 通信

 

客户端

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

通信

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

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

  • 丢包, 丢全部还一部分?

           只能丢全部

优点: 效率高

 

 

UDP通信流程

 

       

 

 

 

服务端实现代码

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<arpa/inet.h>
#include<stdlib.h>

int main(int argc, const char* argv[]){
	//创建套接字
	//tcp,ued通信,则第一个参数是AF_INET
	//第三个参数是协议,这里传0
	int 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;
}

 

 

 

客户端实现代码

程序没有什么错误输出,很粗糙的代码

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<arpa/inet.h>
#include<stdlib.h>

int 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改成整型ip
	inet_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和recvfrom
		recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
		printf("recv buf:%s\n", buf);
	}

	close(fd);
	return 0;
}

 

运行截图:

 

 

 

tcp和udp的使用场景

 

tcp使用场景

  • 对数据安全性要求高的时候
    • 登录数据的传输
    • 文件传输
  • http协议的底层传输使用的是
    • 传输层协议 - tcp

 

udp使用场景

  • 效率高 - 实时性要求比较高
    • 视频聊天
    • 通话
    • 屏幕共享软件
  • 有实力的大公司在做通讯工具时
    • 使用upd,在应用层自定义协议, 做数据校验

 

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值