本系列文章導航: 手把手寫C++服務器(0):專欄文章-匯總導航【更新中】
前言:connect創建的時候是默認阻塞模式的,但是現實情況里可能會因為網路差、中間代理服務器、網關等因素造成連接速度慢,此時,在阻塞模式下,程式會阻塞在connect中很久,因此,在實際的專案中,我們一般傾向于使用異步connect技術,學習如何利用IO復用技術設定異步connect,不僅能為后面高并發多執行緒打下基礎,也是后端開發面試必知必會的知識點,
目錄
預備知識
1、connect函式
函式回傳:
2、getsocketopt方法
getsockopt、setsockopt
常用選項選講:
3、IO復用之select
函式回傳
引數詳解
重要結構體詳解
使用流程
正式開始
1、代碼流程
2、客戶端代碼
3、服務端代碼
4、實驗效果
參考
預備知識
1、connect函式
客戶端使用connect()與服務端建立連接:
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t addrlen);
sockfd引數由socket系統呼叫回傳一個socket, serv_addr引數是服務器監聽的socket地址, addrlen引數則指定這個地址的長度,
函式回傳:
connect成功時回傳0, 一旦成功建立連接, sockfd就唯一地標識了這個連接, 客戶端就可以通過讀寫sockfd來與服務器通信, connect失敗則回傳-1并設定errno, 其中兩種常見的errno是ECONNREFUSED和ETIMEDOUT, 它們的含義如下:
- ECONNREFUSED, 目標埠不存在, 連接被拒絕,
- ETIMEDOUT, 連接超時,
2、getsocketopt方法
getsockopt、setsockopt
讀取和設定檔案描述符的屬性和方法,
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t* restrict option_len);
level:指定屬性,如IPv4、IPv6、TCP等
具體選項引數含義:getsockopt
#define SOL_IP 0
#define SOL_IPX 256
#define SOL_AX25 257
#define SOL_ATALK 258
#define SOL_NETROM 259
#define SOL_TCP 6
#define SOL_UDP 17
#define SOL_SOCKET 0xffff
常用選項選講:
1、SO_REUSEADDR
當TCP連接處于TIME_WAIT狀態的時候,SO_REUSEADDR來強制使用被處于TIME_WAIT狀態的連接占用的socket地址,使該地址能立即被重用,
2、SO_RCVBUF
TCP接識訓沖區大小
3、SO_SNDBUF
TCP發送緩沖區大小
4、SO_RCVLOWAT
TCP接識訓沖區低水位標記,被I/O復用系統呼叫用來判斷socket是否可寫,
5、SO_SNDLOWAT
TCP發送緩沖區低水位標記,被I/O復用系統呼叫用來判斷socket是否可寫,
6、SO_LINGER
控制close系統呼叫在關閉TCP連接時的行為
3、IO復用之select
select的作用是在一段指定的時間內,監聽用戶感興趣的檔案描述符上的可讀、可寫、例外等事件,函式原型如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
函式回傳
- select成功時回傳就緒檔案描述符的總數;
- 如果在超時時間內沒有任何檔案描述符就緒,select將回傳0;
- select失敗時回傳-1并設定errno,;
- 如果在select等待期間,程式接收到信號,select立即回傳-1,并將errno設定為EINTR,
引數詳解
- nfds:指定被監聽檔案描述符總數,通常被設定為select監聽所有檔案描述符中的最大值+1,
- readfds:可讀事件對應檔案描述符集合,
- writefds:可寫事件對應檔案描述符集合,
- exceptfds:例外事件對應檔案描述符集合,
- timeout:設定select超時時間,
重要結構體詳解
readfds、writefds、exceptfds都是fd_set結構體,timeout是timeval結構體,這里詳解一下這兩個結構體,
1、fd_set
fd_set結構體定義比較復雜,涉及到位操作,比較復雜,所以通常用宏來訪問fd_set中的位,
#include <sys/select.h>
FD_ZERO(fd_set* fdset); // 清除fdset中的所有位
FD_SET(int fd, fd_set* fdset); // 設定fdset中的位
FD_CLR(int fd, fd_set* fdset); // 清除fdset中的位
int FD_ISSET(int fd, fd_set* fdset); // 測驗fdset的位fd是否被設定
- FD_ZERO用來清空檔案描述符組,每次呼叫select前都需要清空一次,
- FD_SET添加一個檔案描述符到組中,FD_CLR對應將一個檔案描述符移出組中,
- FD_ISSET檢測一個檔案描述符是否在組中,我們用這個來檢測一次select呼叫之后有哪些檔案描述符可以進行IO操作,
2、timeval
struct timeval {
long tv_sec; // 秒數
long tv_usec; // 微妙數
};
使用流程
綜上所述,我們一般的使用流程是:
- 準備作業——定義readfds、timeval等
- 使用FD_ZERO清零,使用FD_SET設定檔案描述符,因為事件發生后,檔案描述符集合都將被內核修改,
- 呼叫select
- 使用FD_ISSET檢測檔案描述符是否在組中
正式開始
1、代碼流程
- 套用socket一般框架,
- 將創建的socket設定為非阻塞模式,
- 連接服務器,
- 呼叫select監聽連接失敗的socket上的可寫事件,
- 呼叫getsockopt讀取錯誤碼并清除socket上的錯誤,
2、客戶端代碼
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include <fcntl.h>
// #define SEND_DATA "NO BLOCKING MODEL"
#define SEND_DATA "hello"
using namespace std;
// 將檔案描述符設定為非阻塞模式
int setnoblocking(int fd) {
// 獲取檔案描述符舊的狀態標志
int old_option = fcntl(fd, F_GETFL);
// 設定非阻塞標志
int new_option = old_option | O_NONBLOCK;
// 設定非阻塞模式
if (fcntl(fd, F_SETFL, new_option) == -1) {
// std::cout << "set no blocking model is error" << std::endl;
return -1;
}
// 回傳檔案描述符舊的狀態標志,以便日后恢復改狀態標志
return old_option;
}
// 設定非阻塞connect
int set_unblock_connect(int port, int time = 10) {
//創建socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1) {
std::cout << "create client error" << std::endl;
return -1;
}
//向服務器(特定的IP和埠)發起請求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每個位元組都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
serv_addr.sin_port = htons(port); //埠
// 將clientfd設定為非阻塞模式
int oldconnectfd = setnoblocking(clientfd);
if (oldconnectfd == -1) {
std::cout << "set no block model is error" << std::endl;
close(clientfd);
return -1;
}
int connectfd = connect(clientfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (connectfd == 0) { // 連接成功
std::cout << "connect success!" << std::endl;
// 恢復clientfd屬性
fcntl(clientfd, F_SETFL, oldconnectfd);
return clientfd;
} else if (errno != EINPROGRESS) {
// 只有EINPROGRESS模式才表示連接還在進行
std::cout << "connect is error!" << std::endl;
return -1;
}
fd_set readfds;
fd_set writefds;
struct timeval timeout;
FD_ZERO(&readfds);
FD_SET(clientfd, &writefds);
timeout.tv_sec = time;
timeout.tv_usec = 0;
int ret = select(clientfd + 1, nullptr, &writefds, nullptr, &timeout);
if (ret <= 0) {
// select設定出錯或超時
std::cout << "set select is error" << std::endl;
close(clientfd);
return -1;
}
if (!FD_ISSET(clientfd, &writefds)) {
std::cout << "no events on clientfd found!" << std::endl;
close(clientfd);
return -1;
}
int error = 0;
socklen_t length = static_cast<socklen_t>(sizeof error);
// 使用getsockopt獲取并清除sockfd上面的錯誤
if(getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0) {
std::cout << "get socket option is error" << std::endl;
close(clientfd);
return -1;
}
if (error != 0) {
std::cout << "connect is fail after select with error " << error << std:: endl;
close(clientfd);
return -1;
}
// 連接成功
std::cout << "connect is success after select!" << std::endl;
fcntl(clientfd, F_SETFL, oldconnectfd);
return clientfd;
}
int main(int argc, char* argv[]) {
if (argc <= 1) {
printf("error! please input port!\n");
return 1;
}
// 第一個入參是埠
int port = atoi(argv[1]);
// 設定非阻塞connect
int clientfd = set_unblock_connect(port, 10);
if (clientfd < 0) {
std::cout << "setting unblock connect is error" << std::endl;
return 0;
}
// while (true) {
// char recvbuf[32] = {0};
// // 非阻塞模式下,無論是否有資料,都不會阻塞程式
// int ret = recv(clientfd, recvbuf, 32, 0);
// if (ret > 0) {
// std::cout << "recv data successfully!" << std::endl;
// } else if(ret == 0) {
// std::cout << "socket is closed!" << std::endl;
// break;
// } else {
// if (errno == EWOULDBLOCK) {
// std::cout << "no data avaliable" << std::endl;
// } else if (errno == EINTR) {
// std::cout << "recv data interruptered by signal" << std::endl;
// } else {
// break;
// }
// }
// }
close(clientfd);
return 0;
}
3、服務端代碼
// 設定非阻塞模式
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <assert.h>
#include <errno.h>
#include <netinet/in.h>
#include <iostream>
int main(int argc, char* argv[]) {
if (argc <= 1) {
printf("error! please input port!\n");
return 1;
}
// 創建監聽socket
int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(listenfd);
int port = atoi(argv[1]);
// 將套接字和IP、埠系結
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每個位元組都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
serv_addr.sin_port = htons(port); //埠
if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
std::cout << "bind listen socket is error" << std::endl;
close(listenfd);
return -1;
}
// 進入監聽狀態,等待用戶發起請求
if (listen(listenfd, SOMAXCONN) == -1) {
std::cout << "listen error" << std::endl;
close(listenfd);
return -1;
}
// 接收客戶端請求
while (true) {
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);
if (clientfd != -1) {
std::cout << "accept a client connection" << std::endl;
} else {
std::cout << "accept connection error!" << std::endl;
}
}
close(listenfd);
return 0;
}
4、實驗效果

參考
- 《C++服務器開發精髓》
- 《Linux高性能服務器編程》
- 手把手寫C++服務器(35):手撕代碼——高并發高QPS技識訓石之非阻塞send【萬字長文】_沉迷單車的追風少年-CSDN博客
- 手把手寫C++服務器(34):高并發高吞吐IO秘密武器——epoll池化技術【兩萬字長文】_沉迷單車的追風少年-CSDN博客
- 手把手寫C++服務器(0):專欄文章-匯總導航【更新中】_沉迷單車的追風少年-CSDN博客
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/304538.html
標籤:其他
下一篇:4w字HTML5知識全解
