370 likes | 595 Views
架构平台部 < 网络通讯 >培训. 讲师: nekeyzhong 日期:2009年 8 月 21 日. 课程简介. 网络通讯,socket实现tcp,udp。以及select,epoll等功能函数应用. 背景知识回顾. OSI 七层模型: tcp/ip 五层模型:. tcp/ip 四层模型:. Unix基本Socket API介绍1. Unix基本Socket API介绍2. // 客户端 struct sockaddr_in servaddr; int sockfd = socket(AF_INET, SOCK_STREAM, 0);
E N D
架构平台部<网络通讯>培训 讲师:nekeyzhong 日期:2009年8月21日
课程简介 • 网络通讯,socket实现tcp,udp。以及select,epoll等功能函数应用
背景知识回顾 OSI 七层模型: tcp/ip 五层模型: tcp/ip 四层模型:
// 客户端 struct sockaddr_in servaddr; int sockfd = socket(AF_INET, SOCK_STREAM, 0); servaddr.sin_family = AF_INET; servaddr.sin_port =htons(18000); servaddr.sin_addr.s_addr = inet_addr("192.168.1.100"); connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); write(sockfd,"hello",strlen("hello")); memset(szRecv,0,sizeof(szRecv)); int n = read(sockfd,szRecv,sizeof(szRecv)); printf("%s\n",szRecv); close(sockfd); 简单的TCP客户端代码
简单的TCP服务器代码 int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server; server.sin_addr.s_addr = INADDR_ANY; servaddr.sin_family = AF_INET; servaddr.sin_port = htons(18000); bind(listenfd, &servaddr, sizeof(servaddr)); listen(listenfd, 10); while(1){ clilen = sizeof(cliaddr); int newclientfd = accept(listenfd, &cliaddr, &clilen); n = read(newclientfd ,sRecv,sizeof(sRecv)); if(n == 0) close(newclientfd ); n = write(newclientfd , sRecv, n); close(newclientfd ); }
简单的UDP客户端代码 char buf[1024]; servaddr.sin_family = AF_INET; struct sockaddr_in server; server.sin_port = htons(8888); server.sin_addr.s_addr = inet_addr("192.168.1.100"); int sockfd = socket(AF_INET,SOCK_DGRAM,0); while(1){ strcpy(buf,"hello"); int ret = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&server,sizeof(struct sockaddr_in)); socklen_t len = sizeof(struct sockaddr_in); ret = recvfrom(sockfd,buf,1024,0,(struct sockaddr *)&server,(socklen_t*)&len); printf("redv:%s\n",buf); }
简单的UDP服务器代码 struct sockaddr_in server ; servaddr.sin_family = AF_INET; server.sin_port = htons(8888); server.sin_addr.s_addr = INADDR_ANY; struct sockaddr_in client; sockfd = socket(AF_INET,SOCK_DGRAM,0); bind(sockfd,(struct sockaddr *)&server,sizeof(struct sockaddr_in)); int iclientlen = sizeof(struct sockaddr_in); while(1){ int recvlen= recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&client, (socklen_t*)&iclientlen); sendto(sockfd,buf,recvlen,0,(struct sockaddr *)&client,sizeof(struct sockaddr_in)); }
UDP的显式connect • udp也可以使用connect来指明对方ip,port • 使用connect后,udp可以使用write,read等函数发送数据,无需指明对方地址。 • 只要知道对方的ip,port就可以发送数据给对方。 • recvfrom 可能收到任何对端的数据,不一定是你期待的对端。例如客户端调用recvfrom得到的数据可能不是sendto所指明的服务器发送的。 • 使用connect可以指明对端,read时只能收到此对端的数据。 • 由于udp可以使用一个socket像任何对端发送数据,也可以使用一个socket从任何对端接收数据,所以它只需要一个socket。
TCP与UDP的差异 不可靠的传送 64K,大了要分包 一个socket可以处理所有client,server 面向报文的数据发送,IP报文不会合并或拆分 无连接概念,只要ip,port即可通讯。 无流控,快的淹没慢的。 • 可靠的传送 • 报文无大小限制 • 一个socket对应一对client,server • 流式数据发送,IP报文可合并或拆分。 • 需要建立连接,并维护。 • 有流控,不会淹没 流式数据在接收时面临怎样的问题?
TCP的连接过程(3次握手) • 3次握手 主动方 消息 被动方 connect() --SYN--> SYN_SEND SYN_RECV <-- SYN+ACK-- ESTABLISHED -- ACK --> ESTABLISHED • 失败流程: SYN ----------------> 关闭的端口 <---------RST----
TCP状态机 状态机 连接管理 定时器 拥塞控制算法 安全
TCP的断接过程 close() ------ FIN -------> FIN_WAIT1 CLOSE_WAIT <----- ACK ------- FIN_WAIT2 close() 调用 <------ FIN ------ LAST_ACK TIME_WAIT ------ ACK -------> (wait...) CLOSED CLOSED
一个工具 tcpdump • tcpdump -X -s 0 port 39111 and host 172.27.196.237
字节序 什么是字节序?字节序由什么决定? 网络字节序是big-endian intel x86是little-endian sender htonl(int) recver ntohl(int)
阻塞与非阻塞的问题 什么是阻塞,非阻塞? connect,accept,read,write 默认情况下,行为均是阻塞的。 read 在未读到数据时不会返回。 write 在数据未完全发完时不会返回。 过多的等待,一个流程大部分时间都在等待,无事可做... 一次只能在一个连接上等待。 怎么解决? 1.单流程并发 2.多流程 3.非阻塞流程
单流程并发(Select 多路监听) listenfd clientfd[128] fd_set allSet; FD_ZERO(&allSet); //每次select之前必须重新设置fd_set FD_SET(listenfd,&allSet); FD_SET(clientfd[0-127],&allSet); //设入所有有效的clientfd if (select(FD_SETSIZE, &read_set,NULL, NULL, NULL) > 0){ for (int i = 0; i < 127; ++i){ if (FD_ISSET(clientfd[i], &read_set)){ ProcessRecv(clientfd[i]); } if (FD_ISSET(listenfd, &read_set)) { Accept_New(listenfd);//将得到的clientfd放入数组 } } select返回0说明什么?read返回0说明什么? 本质上还是阻塞,write仍然存在很多不必要的等待。
多流程并发 • 多流程并发 1.多进程并发 a.主进程accept后,fork一个子进程处理. b.所有进程竞争accept c.主进程accept后,选择一个子进程,将fd交给此进程处理。 2.多线程并发 a.一个线程调用accept产生fd到队列中,其余线程竞争处理此队列中的fd
多流程并发之----多进程并发1 • 多进程并发1 主进程accept后,fork一个子进程处理。子进程只处理一个连接. while(1){ struct sockaddr_in cli_addr; socklen_t cli_len = sizeof (cli_addr); newsockfd = accept (sockfd, (struct sockaddr *) &cli_addr, &cli_len); pid_t pid = fork(); if (pid ==0){ do_work(newsockfd); close(newsockfd); exit(0); } }
多流程并发之----多进程并发2 • 多进程并发2 所有进程竞争accept(惊群现象) bind(listenfd, ...); listen(listenfd); // 创建进程 for (int i = 0; i < forknum; ++i){ pid = fork(); if (pid == 0) break; } // 多个进程竞争accept for (;;){ connfd = accept(listenfd, ...); if(connfd >=0) child_process(listendfd); }
多流程并发之----多进程并发3 • 多进程并发2 选择一个子进程,将fd交给此进程处理 for (;;){ connfd = accept(listenfd, ...); for (i = 0; i < childnum; ++i){ if (child[i].status == 0) // 这个子进程不忙,通过管道传送fd,将任务发给它 write(child[i].pipefd, &connfd,sizeof(connfd)); break; } } 没有竞争,没有惊群,但需要维护复杂的进程状态。
多流程并发之----多线程并发 • 多线程并发 线程A accept描述符fd到一个队列,线程N1,N2。。。从队列竞争得到fd. 临界资源,需要保护
多流程的缺点 • 各流程之间竞争cpu资源,切换代价大。 • 多少个流程是合适的? • 单个流程阻塞严重时,需要更多的流程,更多的系统资源。 • 性能有上限,总能力=单个流程能力*流程数 怎么解决? 非阻塞
非阻塞socket • read 将OS缓存中的数据读出,无数据时不会等待。 • write 将数据写到OS缓存后马上返回,能写多少写多少。 应用程序调用 read write OS系统缓存 read_buf write_buf 网络传输 物理网络 设置非阻塞: int fdctl = fcntl(socket,F_GETFL); fcntl(socketfd,F_SETFL,fdctl | O_NONBLOCK); 设置/获取系统socket缓存大小: setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, (const void *)&iOptVal, iOptLen); getsockopt(...)
非阻塞socket • 由于没有阻塞点,只需要一个流程即可。 • write不保证数据全部能否发送完毕,所以需要程序需要自己保存未发完数据,等待下次发送。 • 非阻塞特性可以通过accpet继承下来。 int iWLen = write(clientfd,szBuff,iDataLen); if(iWLen < iDataLen){ append_to_buffer(szBuff+iWLen,iDataLen-iWLen ); } 复杂性在于需要对每一个链接维护状态和残余数据。
非阻塞socket返回值 send > 0 等于申请发送的长度,一切OK 小于申请发送的长度,缓冲满,部分数据未发送。 send < 0 errno等于EAGAIN,此次调用失败,socket正常 errno等于其它值,socket异常,需要关闭。 recv < 0 errno等于EAGAIN,此次调用失败,socket正常 errno等于其它值,socket异常,需要关闭。 recv=0 对端关闭。
比select更快的epoll • Epoll 一种新的I/O多路复用技术。 • 传统的select以及poll的效率会因为在线人数的线形递增而导致呈二次乃至三次方的下降,这些直接导致了网络服务器可以支持的人数有了个比较明显的限制。 • select 管理的描述符最多1024个。 • 基于性能问题,使用epoll替代select。
Epoll 函数 • 函数声明:int epoll_create(int size)该函数生成一个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:指向epoll_event的指针; 例如: ev.data.fd = newSocket; ev.events = EPOLLIN | EPOLLERR | EPOLLOUT; • 如果调用成功返回0,不成功返回-1
Epoll 函数 • 函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)该函数用于轮询I/O事件的发生; 参数:epfd:由epoll_create 生成的epoll专用的文件描述符;epoll_event:用于回传代处理事件的数组;maxevents:每次能处理的事件数;timeout:等待I/O事件发生的超时值; 返回发生事件数。
events字段 • EPOLLIN:表示对应的文件描述符可以读 • EPOLLOUT:表示对应的文件描述符可以写 • EPOLLPRI:表示对应的文件描述符有紧急的数据可读 • EPOLLERR:表示对应的文件描述符发生错误 • EPOLLHUP:表示对应的文件描述符被挂断 • EPOLLET/EPOLLLT:表示事件触发模式 Edge Triggered (ET) Level Triggered (LT)
对比 FD_SET(socketfd, &rfds); FD_SET(socketfd, &wfds); epoll_ctl(epollfd, EPOLL_CTL_ADD, newSocket, &ev) select(maxfd+1, &rfds, NULL, NULL, &timeout) epoll_wait(epollfd, events, MaxfdNum,timeout) FD_ISSET(socket, rfds) FD_ISSET(socket, wfds) if(events[i]->events &EPOLLOUT)
example if(events[i].data.fd==listenfd) accept(listenfd...); else if(events[i].events & EPOLLIN) read(events[i].data.fd... else if(events[i].events & EPOLLOUT) write(events[i].data.fd... else { close(events[i].data.fd); epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd, &ev); } struct epoll_event ev,events[MAXSOCKETNUM]; epfd=epoll_create(MAXSOCKETNUM); listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd,(sockaddr *) serveraddr, sizeof(serveraddr)); listen(listenfd, 1024); ev.data.fd=listenfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd, ev); while(1){ nfds=epoll_wait(epfd,events,MAXSOCKETNUM,timeout); for(i=0;i<nfds;++i) {
推荐书目: • <Unix網絡編程 > 淸華大學詘版社
课后作业 • 作业内容: 做一个http服务器,提供下载文件功能。 要求:在浏览器地址栏输入:http://172.16.16.16:8080/htdocs/aaa.bmp,能够下载一个文件 http://172.16.16.16:8080/htdocs/aaa.html,能够提供网页浏览 http协议自己找资料 • 作业要求: • 性能要尽可能的高,选用合理的模型 • 作业例子程序,附件