一、關於socket通信
服務器端工作流程:
- 調用 socket() 函數創建套接字 用 bind() 函數將創建的套接字與服務端IP地址綁定
- 調用listen()函數監聽socket() 函數創建的套接字,等待客戶端連接 當客戶端請求到來之后
- 調用 accept()函數接受連接請求,返回一個對應於此連接的新的套接字,做好通信准備
- 調用 write()/read() 函數和 send()/recv()函數進行數據的讀寫,通過 accept() 返回的套接字和客戶端進行通信 關閉socket(close)
客戶端工作流程:
- 調用 socket() 函數創建套接字
- 調用 connect() 函數連接服務端
- 調用write()/read() 函數或者 send()/recv() 函數進行數據的讀寫
- 關閉socket(close)
二、用select實現服務器端編程:
select函數樓主在之前文章中(select函數用法)已經提及,不在多做綴述。下面貼上服務器端代碼servce.c
#include <stdio.h> #include <netinet/in.h> //for souockaddr_in #include <sys/types.h> #include <sys/socket.h> #include <errno.h> #include <stdlib.h> #include <arpa/inet.h> //for select #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/select.h> #include <strings.h> //for bzero #include <string.h> #define BUFF_SIZE 1024 #define backlog 7 #define ser_port 11277 #define CLI_NUM 3 int client_fds[CLI_NUM]; int main(int agrc,char **argv) { int ser_souck_fd; int i; char input_message[BUFF_SIZE]; char resv_message[BUFF_SIZE]; struct sockaddr_in ser_addr; ser_addr.sin_family= AF_INET; //IPV4 ser_addr.sin_port = htons(ser_port); ser_addr.sin_addr.s_addr = INADDR_ANY; //指定的是所有地址 //creat socket if( (ser_souck_fd = socket(AF_INET,SOCK_STREAM,0)) < 0 ) { perror("creat failure"); return -1; } //bind soucket if(bind(ser_souck_fd, (const struct sockaddr *)&ser_addr,sizeof(ser_addr)) < 0) { perror("bind failure"); return -1; } //listen if(listen(ser_souck_fd, backlog) < 0) { perror("listen failure"); return -1; } //fd_set fd_set ser_fdset; int max_fd=1; struct timeval mytime; printf("wait for client connnect!\n"); while(1) { mytime.tv_sec=27; mytime.tv_usec=0; FD_ZERO(&ser_fdset); //add standard input FD_SET(0,&ser_fdset); if(max_fd < 0) { max_fd=0; } //add serverce FD_SET(ser_souck_fd,&ser_fdset); if(max_fd < ser_souck_fd) { max_fd = ser_souck_fd; } //add client for(i=0;i<CLI_NUM;i++) //用數組定義多個客戶端fd { if(client_fds[i]!=0) { FD_SET(client_fds[i],&ser_fdset); if(max_fd < client_fds[i]) { max_fd = client_fds[i]; } } } //select多路復用 int ret = select(max_fd + 1, &ser_fdset, NULL, NULL, &mytime); if(ret < 0) { perror("select failure\n"); continue; } else if(ret == 0) { printf("time out!"); continue; } else { if(FD_ISSET(0,&ser_fdset)) //標准輸入是否存在於ser_fdset集合中(也就是說,檢測到輸入時,做如下事情) { printf("send message to"); bzero(input_message,BUFF_SIZE); fgets(input_message,BUFF_SIZE,stdin); for(i=0;i<CLI_NUM;i++) { if(client_fds[i] != 0) { printf("client_fds[%d]=%d\n", i, client_fds[i]); send(client_fds[i], input_message, BUFF_SIZE, 0); } } } if(FD_ISSET(ser_souck_fd, &ser_fdset)) { struct sockaddr_in client_address; socklen_t address_len; int client_sock_fd = accept(ser_souck_fd,(struct sockaddr *)&client_address, &address_len); if(client_sock_fd > 0) { int flags=-1; //一個客戶端到來分配一個fd,CLI_NUM=3,則最多只能有三個客戶端,超過4以后跳出for循環,flags重新被賦值為-1 for(i=0;i<CLI_NUM;i++) { if(client_fds[i] == 0) { flags=i; client_fds[i] = client_sock_fd; break; } } if (flags >= 0) { printf("new user client[%d] add sucessfully!\n",flags); } else //flags=-1 { char full_message[]="the client is full!can't join!\n"; bzero(input_message,BUFF_SIZE); strncpy(input_message, full_message,100); send(client_sock_fd, input_message, BUFF_SIZE, 0); } } } } //deal with the message for(i=0; i<CLI_NUM; i++) { if(client_fds[i] != 0) { if(FD_ISSET(client_fds[i],&ser_fdset)) { bzero(resv_message,BUFF_SIZE); int byte_num=read(client_fds[i],resv_message,BUFF_SIZE); if(byte_num > 0) { printf("message form client[%d]:%s\n", i, resv_message); } else if(byte_num < 0) { printf("rescessed error!"); } //某個客戶端退出 else //cancel fdset and set fd=0 { printf("clien[%d] exit!\n",i); FD_CLR(client_fds[i], &ser_fdset); client_fds[i] = 0; // printf("clien[%d] exit!\n",i); continue; //這里如果用break的話一個客戶端退出會造成服務器也退出。 } } } } } return 0; }
select實現多路復用,多路復用,顧名思義,就是說各做各的事,標准輸入事件到來,有相關函數處理。服務器處理服務器的事件,客戶端到來時有相關函數對其進行處理,通過select遍歷各fd的讀寫情況,就不用擔心阻塞了。
三、用epoll實現客戶端編程:
1、客戶端程序(epoll_client.c):
#include<stdio.h> #include<stdlib.h> #include<netinet/in.h> #include<sys/socket.h> #include<arpa/inet.h> #include<string.h> #include<unistd.h> #include <sys/epoll.h> #include <errno.h> #include <fcntl.h> #define BUFFER_SIZE 1024 int main(int argc, const char * argv[]) { int i,n; int connfd,sockfd; struct epoll_event ev,events[20]; //ev用於注冊事件,數組用於回傳要處理的事件 int epfd=epoll_create(256);//創建一個epoll的句柄,其中256為你epoll所支持的最大句柄數 struct sockaddr_in client_addr; struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(11277); server_addr.sin_addr.s_addr =INADDR_ANY; bzero(&(server_addr.sin_zero), 8); int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0); ev.data.fd=server_sock_fd;//設置與要處理的事件相關的文件描述符 ev.events=EPOLLIN|EPOLLET;//設置要處理的事件類型 epoll_ctl(epfd,EPOLL_CTL_ADD,server_sock_fd,&ev);//注冊epoll事件 if(server_sock_fd == -1) { perror("socket error"); return 1; } char recv_msg[BUFFER_SIZE]; char input_msg[BUFFER_SIZE]; if(connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == 0) { for(;;) { int nfds=epoll_wait(epfd,events,20,500);//等待epoll事件的發生 for(i=0;i<nfds;++i) { if(events[i].events&EPOLLOUT) //有數據發送,寫socket { bzero(input_msg, BUFFER_SIZE); fgets(input_msg, BUFFER_SIZE, stdin); sockfd = events[i].data.fd; write(sockfd, recv_msg, n); ev.data.fd=sockfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev); } else if(events[i].events&EPOLLIN)//有數據到來,讀socket { bzero(recv_msg, BUFFER_SIZE); if((n = read(server_sock_fd, recv_msg, BUFFER_SIZE)) <0 ) { printf("read error!"); } ev.data.fd=server_sock_fd; ev.events=EPOLLOUT|EPOLLET; printf("%s\n",recv_msg); } } } } return 0; }
2、關於epoll函數:
相比於select,epoll最大的好處在於它不會隨着監聽fd數目的增長而降低效率。因為在內核中的select實現中,它是采用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。並且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE 1024
表示select最多同時監聽1024個fd
一共三個函數:
1、 int epoll_create (int size); 創建一個epoll的句柄
size用來告訴內核這個監聽的數目一共有多大。這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當創建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。
2、 int epoll_ctl (int epfd , int op, int fd, struct epoll_event *event);
epoll的事件注冊函數,它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd
第四個參數是告訴內核需要監聽什么事,struct epoll_event結構如下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
events可以是以下幾個宏的集合:
- EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
- EPOLLOUT:表示對應的文件描述符可以寫;
- EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
- EPOLLERR:表示對應的文件描述符發生錯誤;
- EPOLLHUP:表示對應的文件描述符被掛斷;
- EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
- EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產生,類似於select()調用。
參數events用來從內核得到事件的集合,
maxevents告之內核這個events有多大,這個 maxevents的值不能大於創建epoll_create()時的size,
參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。
使用步驟:
<1>首先通過create_epoll(int maxfds)來創建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之后的所有操作將通過這個句柄來進行操作。在用完之后,記得用close()來關閉這個創建出來的epoll句柄。
<2>然后每一幀的調用epoll_wait (int epfd, epoll_event events, int max events, int timeout) 來查詢所有的網絡接口。
<3>kdpfd為用epoll_create創建之后的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之后,epoll_events里面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket句柄數。最后一個timeout是 epoll_wait的超時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件范圍,為任意正整數的時候表示等這么長的時間,如果一直沒有事件,則返回。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。 epoll_wait返回之后應該是一個循環,遍歷所有的事件。
基本上都是如下的框架:
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 { //其他的處理 } } }