這一篇文章主要是理清服務器和客戶端的建立通信的流程,整個通信是在網絡層(即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
{
//其他的處理
}
}
}
因爲讀寫是一個週而復始的過程,那麼服務器讀了之後下一時刻就是將數據寫回去