網絡io與select
我們知道網絡IO模型一共有5種,這里我們主要討論同步IO和select多路復用的情況。
我們先從一個簡單的TCP服務器的代碼出發,來討論一下這個是怎么實現的。
一個十分簡單的TCP服務器
一個簡單的TCP的服務器的建立流程是這樣
- 建立SOCKET
- 綁定端口
- 監聽
- 接受連接
- 接受消息
- 發送消息
- 關閉連接
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>
#define BUFFER_LENGTH 128
int main()
{
//socket有兩個參數,第一個參數指定我們要使用IPV4,還是IPV6,第二個參數表明我們要使用套接字類型,這里我們使用的是流格式的套接字,第三個參數就是我們需要使用傳輸協議
//這里使用0,表示讓系統自動推導我們需要使用的傳輸協議。
int listenfd= socket(AF_INET,SOCK_STREAM,0);
//如果返回值為-1,說明我們創建SOCKET失敗,直接返回。
if (listenfd==-1)
{
return -1;
}
//我們需要綁定的信息
struct sockaddr_in serveraddr;
//使用IPV4
serveraddr.sin_family=AF_INET;
//我們需要綁定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有網卡的所有IP段都可以連接到我們的創建的TCP服務器上。
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
//我們需要綁定的端口,這里我們綁定的端口為9999
serveraddr.sin_port=htons(9999);
//第一個參數我們創建的套接字,第二個是我們填寫的綁定信息,最后是我們的綁定信息結構體的大小。
if (-1==bind(listenfd,(const sockaddr*)&serveraddr,sizeof(serveraddr)))
{
return -2;
}
//監聽我們創建的套接字,請求的隊列數量,這里我們填寫為10個
listen(listenfd,10);
//定義客戶端的socket
struct sockaddr_in client;
//客戶端結構體的長度
socklen_t len=sizeof(client);
//等待接受連接
//第一個參數服務器的套接字,第二個接收到的客戶端的socket,第三個函數就是結構體的長度
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
//接受的緩沖區大小
unsigned char buffer[BUFFER_LENGTH]={0};
//收函數
//第一個參數客戶端的套接字,第二個參數,接受的緩沖區,第三個參數緩沖區的大小,第4個參數接收到的字節數
int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
if (ret==0)
{
close(clientfd);
}
printf("buffer: %s , ret : %d\n",buffer,ret);
//發函數
//第一個參數客戶端的套接字,第二個參數,發送的緩沖區,第三個參數發送的字節數,第4個參數實際發送的字節數
ret = send(clientfd,buffer,ret,0);
}
上面的代碼已經把每個函數的參數的作用,還有 參數的意義都已經注釋上了,
運行一下上面的代碼,并使用我們的網絡調試助手,發現我們的客戶端已經可以收發數據了。
現在就遇到了一個問題,如果我們想一直接受和發送數據,我們需要做什么處理那?
可能大多數人都可以想到,我們添加 一個while循環的就可以一直接受數據了,因此我們改變一下我們的代碼。
讓它可以一直收發數據,知道我們的客戶端退出為止。
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>
#define BUFFER_LENGTH 128
int main()
{
//socket有兩個參數,第一個參數指定我們要使用IPV4,還是IPV6,第二個參數表明我們要使用套接字類型,這里我們使用的是流格式的套接字,第三個參數就是我們需要使用傳輸協議
//這里使用0,表示讓系統自動推導我們需要使用的傳輸協議。
int listenfd= socket(AF_INET,SOCK_STREAM,0);
//如果返回值為-1,說明我們創建SOCKET失敗,直接返回。
if (listenfd==-1)
{
return -1;
}
//我們需要綁定的信息
struct sockaddr_in serveraddr;
//使用IPV4
serveraddr.sin_family=AF_INET;
//我們需要綁定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有網卡的所有IP段都可以連接到我們的創建的TCP服務器上。
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
//我們需要綁定的端口,這里我們綁定的端口為9999
serveraddr.sin_port=htons(9999);
//第一個參數我們創建的套接字,第二個是我們填寫的綁定信息,最后是我們的綁定信息結構體的大小。
if (-1==bind(listenfd,(const sockaddr*)&serveraddr,sizeof(serveraddr)))
{
return -2;
}
//監聽我們創建的套接字,請求的隊列數量,這里我們填寫為10個
listen(listenfd,10);
//定義客戶端的socket
struct sockaddr_in client;
//客戶端結構體的長度
socklen_t len=sizeof(client);
//等待接受連接
//第一個參數服務器的套接字,第二個接收到的客戶端的socket,第三個函數就是結構體的長度
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
//接受的緩沖區大小
unsigned char buffer[BUFFER_LENGTH]={0};
while (1)
{
//收函數
//第一個參數客戶端的套接字,第二個參數,接受的緩沖區,第三個參數緩沖區的大小,第4個參數接收到的字節數
int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
if (ret<=0)
{
close(clientfd);
break;
}
printf("buffer: %s , ret : %d\n",buffer,ret);
//發函數
//第一個參數客戶端的套接字,第二個參數,發送的緩沖區,第三個參數發送的字節數,第4個參數實際發送的字節數
ret = send(clientfd,buffer,ret,0);
}
return 0;
}
這樣我們就達到了我們的要求,那么還有新的問題,就是我創建一個TCP server不能只使用一個。
下面就是如果我這個TCPserver想要連接多個客戶端我應該怎么去做?
有兩個可行的方案 ,供我們使用:
- 多線程、多進程的方式
- select,poll,epoll多路復用的方式。
我們首先看一下第一種的方式
多進程的方式,創建一個服務器,實現一個TCP 服務器可以連接多個TCP 客戶端
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>
#define BUFFER_LENGTH 128
int main()
{
unsigned char buffer[BUFFER_LENGTH]={0};
//socket有兩個參數,第一個參數指定我們要使用IPV4,還是IPV6,第二個參數表明我們要使用套接字類型,這里我們使用的是流格式的套接字,第三個參數就是我們需要使用傳輸協議
//這里使用0,表示讓系統自動推導我們需要使用的傳輸協議。
int listenfd= socket(AF_INET,SOCK_STREAM,0);
//如果返回值為-1,說明我們創建SOCKET失敗,直接返回。
if (listenfd==-1)
{
return -1;
}
//我們需要綁定的信息
struct sockaddr_in serveraddr;
//使用IPV4
serveraddr.sin_family=AF_INET;
//我們需要綁定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有網卡的所有IP段都可以連接到我們的創建的TCP服務器上。
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
//我們需要綁定的端口,這里我們綁定的端口為9999
serveraddr.sin_port=htons(9999);
//第一個參數我們創建的套接字,第二個是我們填寫的綁定信息,最后是我們的綁定信息結構體的大小。
if (-1==bind(listenfd,(const sockaddr*)&serveraddr,sizeof(serveraddr)))
{
return -2;
}
//監聽我們創建的套接字,請求的隊列數量,這里我們填寫為10個
listen(listenfd,10);
//定義客戶端的socket
;
while (1)
{
struct sockaddr_in client;
//客戶端結構體的長度
socklen_t len=sizeof(client);
//等待接受連接
//第一個參數服務器的套接字,第二個接收到的客戶端的socket,第三個函數就是結構體的長度
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if (clientfd<0)
{
close(listenfd);
return -3;
}
pid_t id=fork();
if (id<0)
{
perror("fork");
}else if (id==0)
{
close(listenfd);
pid_t idd= fork();
if (idd<0)
{
perror("second fork");
_exit(5);
}
else if (idd==0)
{
while (1)
{
//收函數
//第一個參數客戶端的套接字,第二個參數,接受的緩沖區,第三個參數緩沖區的大小,第4個參數接收到的字節數
int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
if (ret<=0)
{
close(clientfd);
break;
}
printf("buffer: %s , ret : %d\n",buffer,ret);
//發函數
//第一個參數客戶端的套接字,第二個參數,發送的緩沖區,第三個參數發送的字節數,第4個參數實際發送的字節數
ret = send(clientfd,buffer,ret,0);
}
}else{
_exit(6);
}
}
#if 0
//接受的緩沖區大小
unsigned char buffer[BUFFER_LENGTH]={0};
while (1)
{
//收函數
//第一個參數客戶端的套接字,第二個參數,接受的緩沖區,第三個參數緩沖區的大小,第4個參數接收到的字節數
int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
if (ret<=0)
{
close(clientfd);
break;
}
printf("buffer: %s , ret : %d\n",buffer,ret);
//發函數
//第一個參數客戶端的套接字,第二個參數,發送的緩沖區,第三個參數發送的字節數,第4個參數實際發送的字節數
ret = send(clientfd,buffer,ret,0);
}
#endif
}
return 0;
}
然后我們在看看通過第一種方式的多線程的方式來完成我們的TCP服務器
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>
#include<thread>
#define BUFFER_LENGTH 128
void routine(void *arg) {
int clientfd = *(int *)arg;
while (1) {
unsigned char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret == 0) {
close(clientfd);
break;
}
printf("buffer : %s, ret: %d\n", buffer, ret);
ret = send(clientfd, buffer, ret, 0); //
}
}
int main()
{
unsigned char buffer[BUFFER_LENGTH]={0};
//socket有兩個參數,第一個參數指定我們要使用IPV4,還是IPV6,第二個參數表明我們要使用套接字類型,這里我們使用的是流格式的套接字,第三個參數就是我們需要使用傳輸協議
//這里使用0,表示讓系統自動推導我們需要使用的傳輸協議。
int listenfd= socket(AF_INET,SOCK_STREAM,0);
//如果返回值為-1,說明我們創建SOCKET失敗,直接返回。
if (listenfd==-1)
{
return -1;
}
//我們需要綁定的信息
struct sockaddr_in serveraddr;
//使用IPV4
serveraddr.sin_family=AF_INET;
//我們需要綁定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有網卡的所有IP段都可以連接到我們的創建的TCP服務器上。
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
//我們需要綁定的端口,這里我們綁定的端口為9999
serveraddr.sin_port=htons(9999);
//第一個參數我們創建的套接字,第二個是我們填寫的綁定信息,最后是我們的綁定信息結構體的大小。
if (-1==bind(listenfd,(const sockaddr*)&serveraddr,sizeof(serveraddr)))
{
return -2;
}
//監聽我們創建的套接字,請求的隊列數量,這里我們填寫為10個
listen(listenfd,10);
//定義客戶端的socket
;
while (1)
{
struct sockaddr_in client;
//客戶端結構體的長度
socklen_t len=sizeof(client);
//等待接受連接
//第一個參數服務器的套接字,第二個接收到的客戶端的socket,第三個函數就是結構體的長度
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if (clientfd<0)
{
close(listenfd);
return -3;
}
std::thread t{&routine,&clientfd};
t.detach();
}
#if 0
//接受的緩沖區大小
unsigned char buffer[BUFFER_LENGTH]={0};
while (1)
{
//收函數
//第一個參數客戶端的套接字,第二個參數,接受的緩沖區,第三個參數緩沖區的大小,第4個參數接收到的字節數
int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
if (ret<=0)
{
close(clientfd);
break;
}
printf("buffer: %s , ret : %d\n",buffer,ret);
//發函數
//第一個參數客戶端的套接字,第二個參數,發送的緩沖區,第三個參數發送的字節數,第4個參數實際發送的字節數
ret = send(clientfd,buffer,ret,0);
}
#endif
return 0;
}
這樣我們的就完成了我們TCP 單進程多線程服務器的創建,這里會有一個問題:如果我們就想要通過單線程完成我們的可以接受多個客戶端的連接,我們應該怎么去實現那?
在這里,我大概講一下我自己的理解,可能理解的不對,也是剛開始學,我是這樣去理解,當我們使用單線程只能接受一個客戶端的TCP服務器的時候,當我們連接第2個客戶端的時候,ACCEPT是可以接收的,但是沒有辦法進行消息的收發,因此我們就有這樣的一個思路去完成我們的需求,就是通過類似哈希表一樣的結構,有一個新的連接連入我們的時候,我們就插入這個表,然后我們開始輪詢遍歷這個表中的連接,如果有讀寫的事件產生,我們就調用recv和send函數進行調用,完成我們的需求。

類似圖片中的輪詢,下面我們看一下使用select函數的代碼是怎么樣的。
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>
#define BUFFER_LENGTH 128
int main()
{
unsigned char buffer[BUFFER_LENGTH]={0};
int ret=0;
//socket有兩個參數,第一個參數指定我們要使用IPV4,還是IPV6,第二個參數表明我們要使用套接字類型,這里我們使用的是流格式的套接字,第三個參數就是我們需要使用傳輸協議
//這里使用0,表示讓系統自動推導我們需要使用的傳輸協議。
int listenfd= socket(AF_INET,SOCK_STREAM,0);
//如果返回值為-1,說明我們創建SOCKET失敗,直接返回。
if (listenfd==-1)
{
return -1;
}
//我們需要綁定的信息
struct sockaddr_in serveraddr;
//使用IPV4
serveraddr.sin_family=AF_INET;
//我們需要綁定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有網卡的所有IP段都可以連接到我們的創建的TCP服務器上。
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
//我們需要綁定的端口,這里我們綁定的端口為9999
serveraddr.sin_port=htons(9999);
//第一個參數我們創建的套接字,第二個是我們填寫的綁定信息,最后是我們的綁定信息結構體的大小。
if (-1==bind(listenfd,(const sockaddr*)&serveraddr,sizeof(serveraddr)))
{
return -2;
}
//監聽我們創建的套接字,請求的隊列數量,這里我們填寫為10個
listen(listenfd,10);
//定義客戶端的socket
//定義可讀序列和可寫序列
fd_set rfds,wfds,rset,wset;
//清空序列
FD_ZERO(&rfds);
//設置讀的序列
FD_SET(listenfd,&rfds);
//清空可寫的序列
FD_ZERO(&wfds);
int maxfd=listenfd;
while (1)
{
//開始進行序列的賦值,
rset=rfds;
wset=wfds;
//select開始多路服用
//第一個參數是所有文件描述符的范圍,第二個參數監控讀的文件描述符的序列,第三個參數監控寫的文件描述符的序列, 第4個參數監控異常的序列,第5個參數等待的時間,0是指無限等待
int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
//判斷listenfd服務器socket是否被設置,設置代表有效。
if (FD_ISSET(listenfd,&rset))
{
printf("listen --> \n");
struct sockaddr_in client;
socklen_t len = sizeof(client);
//開始接受客戶端得連接
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
//添加可讀列表中
FD_SET(clientfd, &rfds);
//如果客戶端的文件描述符的序號大于最大的文件描述符的編號
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
//開始輪詢監聽的已經連接的客戶端的socket
for (i = listenfd+1; i <= maxfd;i ++) {
//如果有可讀的事件
if (FD_ISSET(i, &rset)) { //
//開始接受信息
ret = recv(i, buffer, BUFFER_LENGTH, 0);
if (ret <= 0) {
close(i);
FD_CLR(i, &rfds);
} else if (ret > 0) {
//打印接受到信息
printf("buffer : %s, ret: %d\n", buffer, ret);
//設置可寫的事件
FD_SET(i, &wfds);
}
//如果有可寫的事件
} else if (FD_ISSET(i, &wset)) {
//開發發送消息
ret = send(i, buffer, ret, 0); //
//清空可寫的事件
FD_CLR(i, &wfds);
//設置可讀事件,因為并沒有斷開連接
FD_SET(i, &rfds);
}
}
}
return 0;
}
這樣就完成了我們的select選擇模型的代碼。今天我們就介紹到這里。
推薦一個零聲學院免費教程,個人覺得老師講得不錯,
分享給大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,
TCP/IP,協程,DPDK等技術內容,點擊立即學習:
服務器
音視頻
dpdk
Linux內核

浙公網安備 33010602011771號