架构平台部< 网络通讯 > 培训
讲师: nekeyzhong日期: 2009 年 8 月 21 日
课程简介
网络通讯,socket实现tcp,udp。以及select,epoll等功能函数应用
背景知识回顾
tcp/ip 四层模型 :
OSI 七层模型 :tcp/ip 五层模型 :
Unix 基本 Socket API 介绍 1
API 作用socket 创建 socket
connect 连接服务器send/write 发送数据recv/read 接收数据bind 绑定监听端口,一般用于服务器listen 监听端口,用于服务器accept 服务器接收客户端连接select 检查可用文件描述符
Unix 基本 Socket API 介绍 2
API 作用sendto 发送 UDP 包recvfrom 接收 UDP 包close 关闭 socket
简单的 TCP 客户端代码
// 客户端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 服务器代码
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 的差异
可靠的传送报文无大小限制一个 socket 对应一对 client,server
流式数据发送, IP 报文可合并或拆分。需要建立连接,并维护。有流控,不会淹没
不可靠的传送64K ,大了要分包一个 socket 可以处理所有 client , server
面向报文的数据发送, IP 报文不会合并或拆分无连接概念,只要 ip,port 即可通讯。无流控,快的淹没慢的。
流式数据在接收时面临怎样的问题?
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 多路监听 )
fd_set allSet;FD_ZERO(&allSet); // 每次 select 之前必须重新设置 fd_set
FD_SET(listenfd,&allSet); FD_SET(clientfd[0-127],&allSet); // 设入所有有效的 clientfdif (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 放入数组 }}
clientfd[128]listenfd
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;}// 多个进程竞争 acceptfor (;;){
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(connf
d));break;
}
}
没有竞争,没有惊群,但需要维护复杂的进程状态。
多流程并发之 ---- 多线程并发
多线程并发 线程 A accept 描述符 fd 到一个队列,线程 N1 , N2 。。。从队列竞争
得到 fd.
临界资源,需要保护
多流程的缺点
各流程之间竞争 cpu资源,切换代价大。多少个流程是合适的?单个流程阻塞严重时,需要更多的流程,更多的系统资源。性能有上限,总能力 = 单个流程能力 * 流程数
怎么解决 ?
非阻塞
非阻塞 socket
read write
read_buf write_buf
物理网络
应用程序调用
OS系统缓存
网络传输
read 将 OS缓存中的数据读出,无数据时不会等待。write 将数据写到 OS缓存后马上返回,能写多少写多少。
设置非阻塞 :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
对端关闭。
recv < 0
errno 等于 EAGAIN ,此次调用失败, socket正常errno 等于其它值, socket 异常,需要关闭。
比 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.data.fd = newSocket; ev.events = EPOLLIN | EPOLLERR | EPOLLOUTev.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
struct epoll_event ev,events[MAXSOCKETNUM];
epfd=epoll_create(MAXSOCKETNUM); listenfd = socket(AF_INET, SOCK_STR
EAM, 0); bind(listenfd,(sockaddr *) serveraddr, siz
eof(serveraddr)); listen(listenfd, 1024);
ev.data.fd=listenfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,listenf
d, ev); while(1 ) {
nfds=epoll_wait(epfd,events,MAXSOCKETNUM,timeout);
for(i=0;i<nfds;++i) {
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); }
推荐书目:
<Unix網絡編程 > 淸華大學詘版社
课后作业作业内容:
做一个 http 服务器,提供下载文件功能。要求:在浏览器地址栏输入: http://172.16.16.16:8080/htdocs/aaa.b
mp ,能够下载一个文件http://172.16.16.16:8080/htdocs/aaa.html ,能够提供网页浏览http协议自己找资料
作业要求:性能要尽可能的高,选用合理的模型
作业例子程序,附件