accept非阻塞方式
C網路程式設計:Server處理多個Client(多程序server方法 和 non-blocking與select結合)
參看基於TCP/UDP的socket程式碼,同一時間Server只能處理一個Client請求:在使用當前連線的socket和client進行互動的時候,不能夠accept新的連線請求。為了使Server能夠處理多個Client請求,常見的方法:
多程序方法(每個子程序單獨處理一個client連線)
在每個accept成功之後,使用fork建立一個子程序專門處理該client的connection,父程序(server)本身可以繼續accept其他新的client的連線請求。具體如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#define DEFAULT_PORT 1984 //預設埠 #define BUFFER_SIZE 1024 //buffer大小
void sigCatcher(int n) {
//printf("a child process dies\n");
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int clientProcess(int new_sock);
int main(int argc, char *argv[]) {
unsigned short int port;
//get port, use default if not set
if (argc == 2) {
port = atoi(argv[1]);
} else if (argc < 2) {
port = DEFAULT_PORT;
} else {
fprintf(stderr, "USAGE: %s [port]\n", argv[0]);
return 1;
}
//create socket
int sock;
if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
perror("socket failed, ");
return 1;
}
printf("socket done\n");
//create socket address and initialize
struct sockaddr_in bind_addr;
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); //設定接受任意地址
bind_addr.sin_port = htons(port); //將host byte order轉換為network byte order
//bind (bind socket to the created socket address)
if ( bind(sock, (struct sockaddr *) &bind_addr, sizeof(bind_addr)) == -1 ) {
perror("bind failed, ");
return 1;
}
printf("bind done\n");
//listen
if ( listen(sock, 5) == -1) {
perror("listen failed.");
return 1;
}
printf("listen done\n");
//handler to clear zombie process
signal(SIGCHLD, sigCatcher);
//loop and respond to client
int new_sock;
int pid;
while (1) {
//wait for a connection, then accept it
if ( (new_sock = accept(sock, NULL, NULL)) == -1 ) {
perror("accept failed.");
return 1;
}
printf("accept done\n");
pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
//這裡是子程序
close(sock); //子程序中不需要server的sock
clientProcess(new_sock); //使用新的new_sock和client進行互動
close(new_sock); //關閉client的連線
exit(EXIT_SUCCESS); //子程序退出
} else {
//這裡是父程序
close(new_sock); //由於new_sock已經交給子程序處理,這裡可以關閉了
}
}
return 0;
}
int clientProcess(int new_sock) {
int recv_size;
char buffer[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
if ( (recv_size = recv(new_sock, buffer, sizeof(buffer), 0)) == -1) {
perror("recv failed");
return 1;
}
printf("%s\n", buffer);
char *response = "This is the response";
if ( send(new_sock, response, strlen(response) + 1, 0) == -1 ) {
perror("send failed");
return 1;
}
return 0;
}
其中的signal(SIGCHLD, sigCatcher)程式碼為了處理zombie process問題:當server程序執行時間較長,且產生越來越多的子程序,當這些子程序執行結束都會成為zombie process,佔據系統的process table。解決方法是在父程序(server程序)中顯式地處理子程序結束之後發出的SIGCHLD訊號:呼叫wait/waitpid清理子程序的zombie資訊。
測試:執行server程式,然後同時執行2個client(telnet localhost 1984),可看到該server能夠很好地處理2個client。
?多程序方法的優點:
每個獨立程序處理一個獨立的client,對server程序來說只需要accept新的連線,對每個子程序來說只需要處理自己的client即可。
?多程序方法的缺點:
子程序的建立需要獨立的父程序資源副本,開銷較大,對高併發的請求不太適合;且一個程序僅處理一個client不能有效發揮作用。另外有些情況下還需要程序間進行通訊以協調各程序要完成的任務。
使用select實現non-blocking socket(single process concurrent server)
blocking socket VS non-blocking socket
預設情況下socket是blocking的,即函式accept(), recv/recvfrom, send/sendto,connect等,需等待函式執行結束之後才能夠返回(此時作業系統切換到其他程序執行)。accpet()等待到有client連線請求並接受成功之後,recv/recvfrom需要讀取完client傳送的資料之後才能夠返回。
non-blocking: by default, sockets are blocking - this means that they stop the function from returning until all data has been transfered.
With multiple connections which may or may not be transmitting data to a server, this would not be very good as connections may have to wait to transmit their data.
設定socket為非阻塞non-blocking
使用socket()建立的socket(file descriptor),預設是阻塞的(blocking);使用函式fcntl()(file control)可設定建立的socket為非阻塞的non-blocking。
#include <unistd.h>
#include <fcntl.h>
sock = socket(PF_INET, SOCK_STREAM, 0);
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
這樣使用原本blocking的各種函式,可以立即獲得返回結果。通過判斷返回的errno瞭解狀態:
?accept():
在non-blocking模式下,如果返回值為-1,且errno == EAGAIN或errno == EWOULDBLOCK表示no connections沒有新連線請求;
?recv()/recvfrom():
在non-blocking模式下,如果返回值為-1,且errno == EAGAIN表示沒有可接受的資料或很在接受尚未完成;
?send()/sendto():
在non-blocking模式下,如果返回值為-1,且errno == EAGAIN或errno == EWOULDBLOCK表示沒有可傳送資料或資料傳送正在進行沒有完成。
?read/write:
在non-blocking模式下,如果返回-1,且errno == EAGAIN表示沒有可讀寫資料或可讀寫正在進行尚未完成。
?connect():
在non-bloking模式下,如果返回-1,且errno = EINPROGRESS表示正在連線。
使用如上方法,可以建立一個non-blocking的server的程式,類似如下程式碼:
int main(int argc, char *argv[]) {
int sock;
if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
perror("socket failed");
return 1;
}
//set socket to be non-blocking
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
//create socket address to bind
struct sockaddr_in bind_addr
...
//bind
bind(...)
...
//listen
listen(...)
...
//loop
int new_sock;
while (1) {
new_sock = accept(sock, NULL, NULL);
if (new_sock == -1 && errno == EAGAIN) {
fprintf(stderr, "no client connections yet\n");
continue;
} else if (new_sock == -1) {
perror("accept failed");
return 1;
}
//read and write
...
}
...
}
純non-blocking程式缺點:如果執行如上程式會發現呼叫accept可以理解返回,但這樣會耗費大量的CPU time,實際中並不會這樣使用。實際中將non-blocking和select結合使用。
non-blocking和select結合使用
select通過輪詢,監視指定file descriptor(包括socket)的變化,知道:哪些ready for reading, 哪些ready for writing,哪些發生了錯誤等。select和non-blocking結合使用可很好地實現socket的多client同步通訊。
select函式:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfd, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
//maxfd: 所有set中最大的file descriptor + 1
//readfds: 指定要偵聽ready to read的file descriptor,可以為NULL
//writefds: 指定要偵聽ready to write的file descriptor,可以為NULL
//errorfds: 指定要偵聽errors的file descriptor,可以為NULL
//timeout: 指定偵聽到期的時間長度,如果該struct timeval的各個域都為0,則相當於完全的non-blocking模式;如果該引數為NULL,相當於block模式;
//select返回total number of bits set in readfds, writefds and errorfds,當timeout的時候返回0,發生錯誤返回-1。
//另外select會更新readfds(儲存ready to read的file descriptor), writefds(儲存read to write的fd), errorfds(儲存error的fd),且更新timeout為距離超時時刻的剩餘時間。
另外,fd_set型別需要使用如下4個巨集進行賦值:
FD_ZERO(fd_set *set); //Clear all entries from the set.
FD_SET(int fd, fd_set *set); //Add fd to the set.
FD_CLR(int fd, fd_set *set); //Remove fd from the set.
FD_ISSET(int fd, fd_set *set); //Return true if fd is in the set.
因此通過如下程式碼可以將要偵聽的file descriptor/socket新增到響應的fd_set中,例如:
fd_set readfds;
FD_ZERO(&readfds);
int sock;
sock = socket(PF_INET, SOCK_STREAM, 0);
FD_SET(sock, &readfds); //將新建立的socket新增到readfds中
FD_SET(stdin, &readfds); //將stdin新增到readfds中
struct timeval型別:
struct timeval {
int tv_sec; //seconds
int tv_usec; //microseconds,注意這裡是微秒不是毫秒,1秒 = 1000, 000微秒
};
因此,使用select函式可以新增希望偵聽的file descriptor/socket到read, write或error中(如果對某一項不感興趣,可以設定為NULL),並設定每次偵聽的timeout時間。
注意如果設定timeout為:
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
相當於每次select立即返回相當於純non-blocking模式;
如果設定timeout引數為NULL,則每次select持續等待到有變化則相當於blocking模式。
使用select和non-blocking實現server處理多client例項:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/time.h>
#define DEFAULT_PORT 1984 //預設埠
#define BUFF_SIZE 1024 //buffer大小
#define SELECT_TIMEOUT 5 //select的timeout seconds
//函式:設定sock為non-blocking mode
void setSockNonBlock(int sock) {
int flags;
flags = fcntl(sock, F_GETFL, 0);
if (flags < 0) {
perror("fcntl(F_GETFL) failed");
exit(EXIT_FAILURE);
}
if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("fcntl(F_SETFL) failed");
exit(EXIT_FAILURE);
}
}
//函式:更新maxfd
int updateMaxfd(fd_set fds, int maxfd) {
int i;
int new_maxfd = 0;
for (i = 0; i <= maxfd; i++) {
if (FD_ISSET(i, &fds) && i > new_maxfd) {
new_maxfd = i;
}
}
return new_maxfd;
}
int main(int argc, char *argv[]) {
unsigned short int port;
//獲取自定義埠
if (argc == 2) {
port = atoi(argv[1]);
} else if (argc < 2) {
port = DEFAULT_PORT;
} else {
fprintf(stderr, "USAGE: %s [port]\n", argv[0]);
exit(EXIT_FAILURE);
}
//建立socket
int sock;
if ( (sock = socket(PF_INET, SOCK_STREAM, 0)) == -1 ) {
perror("socket failed, ");
exit(EXIT_FAILURE);
}
printf("socket done\n");
//in case of 'address already in use' error message
int yes = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int))) {
perror("setsockopt failed");
exit(EXIT_FAILURE);
}
//設定sock為non-blocking
setSockNonBlock(sock);
//建立要bind的socket address
struct sockaddr_in bind_addr;
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); //設定接受任意地址
bind_addr.sin_port = htons(port); //將host byte order轉換為network byte order
//bind sock到建立的socket address上
if ( bind(sock, (struct sockaddr *) &bind_addr, sizeof(bind_addr)) == -1 ) {
perror("bind failed, ");
exit(EXIT_FAILURE);
}
printf("bind done\n");
//listen
if ( listen(sock, 5) == -1) {
perror("listen failed.");
exit(EXIT_FAILURE);
}
printf("listen done\n");
//建立並初始化select需要的引數(這裡僅監視read),並把sock新增到fd_set中
fd_set readfds;
fd_set readfds_bak; //backup for readfds(由於每次select之後會更新readfds,因此需要backup)
struct timeval timeout;
int maxfd;
maxfd = sock;
FD_ZERO(&readfds);
FD_ZERO(&readfds_bak);
FD_SET(sock, &readfds_bak);
//迴圈接受client請求
int new_sock;
struct sockaddr_in client_addr;
socklen_t client_addr_len;
char client_ip_str[INET_ADDRSTRLEN];
int res;
int i;
char buffer[BUFF_SIZE];
int recv_size;
while (1) {
//注意select之後readfds和timeout的值都會被修改,因此每次都進行重置
readfds = readfds_bak;
maxfd = updateMaxfd(readfds, maxfd); //更新maxfd
timeout.tv_sec = SELECT_TIMEOUT;
timeout.tv_usec = 0;
printf("selecting maxfd=%d\n", maxfd);
//select(這裡沒有設定writefds和errorfds,如有需要可以設定)
res = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (res == -1) {
perror("select failed");
exit(EXIT_FAILURE);
} else if (res == 0) {
fprintf(stderr, "no socket ready for read within %d secs\n", SELECT_TIMEOUT);
continue;
}
//檢查每個socket,並進行讀(如果是sock則accept)
for (i = 0; i <= maxfd; i++) {
if (!FD_ISSET(i, &readfds)) {
continue;
}
//可讀的socket
if ( i == sock) {
//當前是server的socket,不進行讀寫而是accept新連線
client_addr_len = sizeof(client_addr);
new_sock = accept(sock, (struct sockaddr *) &client_addr, &client_addr_len);
if (new_sock == -1) {
perror("accept failed");
exit(EXIT_FAILURE);
}
if (!inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip_str, sizeof(client_ip_str))) {
perror("inet_ntop failed");
exit(EXIT_FAILURE);
}
printf("accept a client from: %s\n", client_ip_str);
//設定new_sock為non-blocking
setSockNonBlock(new_sock);
//把new_sock新增到select的偵聽中
if (new_sock > maxfd) {
maxfd = new_sock;
}
FD_SET(new_sock, &readfds_bak);
} else {
//當前是client連線的socket,可以寫(read from client)
memset(buffer, 0, sizeof(buffer));
if ( (recv_size = recv(i, buffer, sizeof(buffer), 0)) == -1 ) {
perror("recv failed");
exit(EXIT_FAILURE);
}
printf("recved from new_sock=%d : %s(%d length string)\n", i, buffer, recv_size);
//立即將收到的內容寫回去,並關閉連線
if ( send(i, buffer, recv_size, 0) == -1 ) {
perror("send failed");
exit(EXIT_FAILURE);
}
printf("send to new_sock=%d done\n", i);
if ( close(i) == -1 ) {
perror("close failed");
exit(EXIT_FAILURE);
}
printf("close new_sock=%d done\n", i);
//將當前的socket從select的偵聽中移除
FD_CLR(i, &readfds_bak);
}
}
}
return 0;
}
編譯並執行如上程式,然後嘗試使用多個telnet localhost 1984連線該server。可以發現各個connection很好地獨立工作。因此,使用select可實現一個程序盡最大所能地處理儘可能多的client。