这一篇文章主要是理清服务器和客户端的建立通信的流程,整个通信是在网络层(即ip协议以及其上的传输层和应用层)。不明白的话需要先了解网络7层模型、对应的报文格式和不同层的封装。
下面主要围绕图11-14讲述并深入探讨
服务器
socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// Example
serverfd = socket(AF_INET, SOCK_STREAM, 0) // 这里返回的描述符仅是部分打开,还不能用于读写
bind
用于将sockaddr与套接字描述符serverfd联系起来。套接字分两种:主动套接字和监听套接字,对应客户端和服务端。
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen); // addrlen = sizeof(sockaddr_in)
listen
调用listen函数告诉内核,描述符是被服务器而不是客户端使用。因为通常情况下,socket函数创建的描述符对应于主动套接字,默认连接一个客户端。而listen将socket函数创建的描述符转化为一个监听套接字,该套接字可以接收来自客户端的连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
accept
等待来自客户端的连接请求到达监听套接字listenfd,在addr中填写客户端的套接字地址,并返回一个已连接描述符,这个描述符可以与客户端通信。
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
监听描述符和已连接描述符之间关系
可以实现并发,每次一个连接请求到达监听描述符,可以fork一个新的进程,它通过已连接描述符与客户端通信。
基于epoll的并发服务器
首先简单介绍一下epoll。epoll是IO多路复用的一种技术,还有就是select和poll。[select最大的不足之处是返回时会重新创建文件描述符集合,因此每次调用都必须重新开始初始化,FD_ZERO和 FD_SET]。下面介绍epoll怎么用
#include <sys/epoll.h>
int epoll_create(int size) //创建epoll实例,并返回与该实例关联的文件描述符
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event);
// epoll_ctl可以向指定的epoll加入或删除文件描述符
// op对fd的操作 EPOLL_CTL_ADD 将fd添加到epfd指向的epoll监听集合中
// EPOLL_CTL_DEL 将fd从epfd指向的epoll监听集合中删除
// EPOLL_CTL_MOD 使用event指定的更新事件修改已有的fd的监听行为
struct epoll_event {
__u32 events;
union {
void *ptr;
int fd;
__u32 u32;
__u64 u64;
} data;
};
// events值 EPOLLET 开启边缘触发
// EPOLLIN 表示对应的文件描述符可以读,用来设置或者检测
// EPOLLOUT 表示对应的文件描述符可以写,用来设置或者检测
// EPOLLPRI 表示存在带外(out-of-band)数据可读
// data是用户私有使用,当接收到请求的时间后,data会通过epoll_wait返回给用户。通常是将event.data.fd设为fd
int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout);
// 调用epoll_wait()时,最多可以有maxevents个事件,超时时间是timeout。成功返回时,events指向每个事件的epoll_event结构体,返回的是事件数
// example
#define MAX_EVENTS 64
int nr_events, i, epfd;
nr_events = epoll_wait (epfd, events, MAX_EVENTS, -1);
if (nr_events < 0) {}
for (i = 0; i < nr_events; i++) {
printf ("event=%ld on fd=%d\n",
events[i].events,
events[i].data.fs);
}
就是将socket编程和epoll结合起来实现并发服务器
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的连接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket
{
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他的处理
}
}
}
因为读写是一个周而复始的过程,那么服务器读了之后下一时刻就是将数据写回去