TCP/IP網路程式設計 學習筆記_13 --基於I/O複用的服務端
前言:前面我們講了多程序的併發服務端,只要有客服端連線請求就會建立新程序,這雖然也是一種解決方案,但建立程序是需要付出極大代價的,這需要大量運算和記憶體空間,而且每個程序間具有獨立的記憶體空間,所以相互間的資料交換也相對複雜(管道)。
本章將討論併發伺服器的第二種實現方法——基於I/O複用的伺服器端構建。
I/O複用
什麼是I/O複用?通俗點講,其實就是一個事件監聽,只是這個監聽的事件一般是I/O操作裡的讀(read)與寫(write),只要發生了監聽的事件它就會響應。注意與一般伺服器的區別,一般伺服器是連線請求先進入請求佇列裡,然後,服務端套接字一個個有序去受理。而I/O複用伺服器是事件監聽,只要對應監聽事件發生就會響應,是屬於併發伺服器的一種。
I/O複用的使用
1,I/O複用的使用其實就是對select函式的使用,說select函式是I/O複用的全部內容也不為過。但這個函式與一般函式不同,它很難使用,我們先來看看它的呼叫順序,分為3步:
步驟一:- 設定檔案描述符,即註冊要監聽的檔案描述符,如監聽標準輸入的檔案描述符0 -> FD_SET(0, &reads)
- 指定監視範圍,Linux上建立檔案物件生成的對應檔案描述符是從0開始遞增的,所以最大監視範圍為最後建立的檔案描述符+1。
- 設定超時,因為select函式是一個阻塞函式,只有監視的檔案描述符發生變化才會返回,設定超時就是為了防止阻塞,如果不想設定超時,則傳遞NULL。
步驟二:
- 呼叫select函式
步驟三:
- 檢視呼叫結果,FD_ISSET(0, &reads)發生變化返回真。
2,再來講講select函式,首先,來看看它的定義:
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
maxfd:監視範圍
readset:監視對應檔案描述符的接收事件,不監視這個事件則傳0
writeset:監視對應檔案描述符的傳輸事件,不監視則傳0
exceptset:監視異常事件,不監視則傳0
timeout:設定超時時間,不設定則傳NULL
返回值:發生錯誤返回-1,超時返回0,發生引數2-4事件返回事件發生的對應檔案描述符。然後,再來看看fd_set陣列,它是一個存有0和1的位陣列,這個陣列中的值0表不監視,1表監視。而每個值對應在陣列中的位置與檔案描述符一一對應。如Linux上的標準輸入的檔案描述符是0,則要監聽這個檔案描述符,就只需把fd_set陣列的fd_set[0]設定為1即可。下面是對fd_set陣列操作的一些巨集:
FD_ZERO(fd_set *fdset):將fd_set變數的所有位初始化為0
FD_SET(int fd, fd_set *fdset):設定檔案描述符fd為監聽狀態
FD_CLR(int fd, fd_set *fdset):取消檔案描述符fd的監聽狀態
FD_ISSET(int fd, fd_set *fdset):檔案描述符fd是否發生相應的監視事件,發生則返回真。3,select函式呼叫示例:
//
// main.cpp
// hello_client
//
// Created by app05 on 15-8-31.
// Copyright (c) 2015年 app05. All rights reserved.
//
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, const char * argv[]) {
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
/*初始化狀態設定*/
FD_ZERO(&reads); //將位陣列reads初始化為0,即不監聽任何檔案描述符
FD_SET(0, &reads); //設定監聽,監聽檔案描述符為0的物件(標準輸入)
while (1)
{
//因為select呼叫後,timeout和初始化狀態會發生變化,所以需要每次迴圈前再初始化一次
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
result = select(1, &temps, 0, 0, &timeout);
if(result == -1)
{
puts("select() error");
break;
}
else if (result == 0)
{
puts("Time-out!");
}
else
{
//監聽的檔案描述符發生接收監聽事件
if (FD_ISSET(0, &temps)) {
str_len = read(0, buf, BUF_SIZE);
buf[str_len] = 0; //字串輸出結束符
printf("message from console: %s", buf);
}
}
}
return 0;
}
實現I/O複用的服務端
用I/O複用的方式實現前面的回聲程式
- 服務端程式碼
//
// main.cpp
// hello_server
//
// Created by app05 on 15-8-31.
// Copyright (c) 2015年 app05. All rights reserved.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handling(char *message);
int main(int argc, const char * argv[]) {
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage: %s <port> \n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while (1)
{
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
//監聽服務端套接字和與客服端連線的服務端套接字的read事件
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if(fd_num == 0)
continue;
if (FD_ISSET(serv_sock, &cpy_reads))//受理客服端連線請求
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else//轉發客服端資料
{
str_len = read(clnt_sock, buf, BUF_SIZE);
if (str_len == 0)//客服端傳送的退出EOF
{
FD_CLR(clnt_sock, &reads);
close(clnt_sock);
printf("closed client: %d \n", clnt_sock);
}
else
{
write(clnt_sock, buf, str_len);
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 客服端程式碼用以前寫的,隨便copy一個來
//
// main.cpp
// hello_client
//
// Created by app05 on 15-7-6.
// Copyright (c) 2015年 app05. All rights reserved.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, const char * argv[]) {
int sock;
char message[BUF_SIZE];
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if(argc != 3)
{
printf("Usage: %s <IP> <port> \n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error");
else
puts("Connected ...............");
while (1) {
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
str_len = write(sock, message, strlen(message));
/*這裡需要迴圈讀取,因為TCP沒有資料邊界,不迴圈讀取可能出現一個字串一次傳送
但分多次讀取而導致輸出字串不完整*/
recv_len = 0;
while (recv_len < str_len) {
recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
if(recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}