導(dǎo)語:本文主要介紹如何從零開始搭建簡單的C++客戶端/服務(wù)器,并進(jìn)行簡單的講解和基礎(chǔ)的壓力測(cè)試演示。該文章相對(duì)比較入門,主要面向了解計(jì)算機(jī)網(wǎng)絡(luò)但未接觸過網(wǎng)絡(luò)編程的同學(xué)。
本文主要分為四個(gè)部分:
搭建C/S:用C++搭建一個(gè)最簡單的,基于socket網(wǎng)絡(luò)編程的客戶端和服務(wù)器
socket庫函數(shù)淺析:基于上一節(jié)搭建的客戶端和服務(wù)器的代碼介紹相關(guān)的庫函數(shù)
搭建HTTP服務(wù)器:基于上一節(jié)的介紹和HTTP工作過程將最開始搭建的服務(wù)器改為HTTP服務(wù)器
壓力測(cè)試入門:優(yōu)化一下服務(wù)器,并使用ab工具對(duì)優(yōu)化前后的服務(wù)器進(jìn)行壓力測(cè)試并對(duì)比結(jié)果
1. 搭建C/S
本節(jié)主要講述如何使用C++搭建一個(gè)簡單的socket服務(wù)器和客戶端。
為了能更加容易理解如何搭建,本節(jié)會(huì)省略許多細(xì)節(jié)和函數(shù)解釋,對(duì)于整個(gè)連接的過程的描述也會(huì)比較抽象,細(xì)節(jié)和解析會(huì)留到之后再講。
服務(wù)端和客戶端的預(yù)期功能
這里要實(shí)現(xiàn)的服務(wù)端的功能十分簡單,只需要把任何收到的數(shù)據(jù)原封不動(dòng)地發(fā)回去即可,也就是所謂的ECHO服務(wù)器。
客戶端要做的事情也十分簡單,讀取用戶輸入的一個(gè)字符串并發(fā)送給服務(wù)端,然后把接收到的數(shù)據(jù)輸出出來即可。
服務(wù)端搭建
將上面的需求轉(zhuǎn)化一下就可以得到如下形式:
while(true)
{
buff = 接收到的數(shù)據(jù);
將buff的數(shù)據(jù)發(fā)回去;
}
當(dāng)然,上面的偽代碼是省略掉網(wǎng)絡(luò)連接和斷開的過程。這個(gè)例子使用的連接形式為TCP連接,而在一個(gè)完整的TCP連接中,服務(wù)端和客戶端通信需要做三件事:
將這些加入偽代碼中,便可以得到如下偽代碼:
while(true)
{
與客戶端建立連接;
buff = 接收到從客戶端發(fā)來的數(shù)據(jù);
將buff的數(shù)據(jù)發(fā)回客戶端;
與客戶端斷開連接;
}
首先需要解決的就是,如何建立連接。
在socket編程中,服務(wù)端和客戶端是靠socket進(jìn)行連接的。服務(wù)端在建立連接之前需要做的有:
創(chuàng)建socket(偽代碼中簡稱為socket()
)
將socket與指定的IP和端口(以下簡稱為port)綁定(偽代碼中簡稱為bind()
)
讓socket在綁定的端口處監(jiān)聽請(qǐng)求(等待客戶端連接到服務(wù)端綁定的端口)(偽代碼中簡稱為listen()
)
而客戶端發(fā)送連接請(qǐng)求并成功連接之后(這個(gè)步驟在偽代碼中簡稱為accept()
),服務(wù)端便會(huì)得到客戶端的套接字,于是所有的收發(fā)數(shù)據(jù)便可以在這個(gè)客戶端的套接字上進(jìn)行了。
而收發(fā)數(shù)據(jù)其實(shí)就是:
在收發(fā)數(shù)據(jù)之后,就需要斷開與客戶端之間的連接。在socket編程中,只需要關(guān)閉客戶端的套接字即可斷開連接。(偽代碼中簡稱為close()
)
將其補(bǔ)充進(jìn)去得到:
sockfd = socket(); // 創(chuàng)建一個(gè)socket,賦給sockfd
bind(sockfd, ip::port和一些配置); // 讓socket綁定端口,同時(shí)配置連接類型之類的
listen(sockfd); // 讓socket監(jiān)聽之前綁定的端口
while(true)
{
connfd = accept(sockfd); // 等待客戶端連接,直到連接成功,之后將客戶端的套接字返回出來
recv(connfd, buff); // 接收到從客戶端發(fā)來的數(shù)據(jù),并放入buff中
send(connfd, buff); // 將buff的數(shù)據(jù)發(fā)回客戶端
close(connfd); // 與客戶端斷開連接
}
這便是socket服務(wù)端的大致流程。詳細(xì)的C++代碼如下所示:
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <sys/types.h>
#include <sys/errno.h>
#include <netinet/in.h>
#include <signal.h>
#define BUFFSIZE 2048
#define DEFAULT_PORT 16555 // 指定端口為16555
#define MAXLINK 2048
int sockfd, connfd; // 定義服務(wù)端套接字和客戶端套接字
void stopServerRunning(int p)
{
close(sockfd);
printf('Close Server\n');
exit(0);
}
int main()
{
struct sockaddr_in servaddr; // 用于存放ip和端口的結(jié)構(gòu)
char buff[BUFFSIZE]; // 用于收發(fā)數(shù)據(jù)
// 對(duì)應(yīng)偽代碼中的sockfd = socket();
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
printf('Create socket error(%d): %s\n', errno, strerror(errno));
return -1;
}
// END
// 對(duì)應(yīng)偽代碼中的bind(sockfd, ip::port和一些配置);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(DEFAULT_PORT);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
{
printf('Bind error(%d): %s\n', errno, strerror(errno));
return -1;
}
// END
// 對(duì)應(yīng)偽代碼中的listen(sockfd);
if (-1 == listen(sockfd, MAXLINK))
{
printf('Listen error(%d): %s\n', errno, strerror(errno));
return -1;
}
// END
printf('Listening...\n');
while (true)
{
signal(SIGINT, stopServerRunning); // 這句用于在輸入Ctrl+C的時(shí)候關(guān)閉服務(wù)器
// 對(duì)應(yīng)偽代碼中的connfd = accept(sockfd);
connfd = accept(sockfd, NULL, NULL);
if (-1 == connfd)
{
printf('Accept error(%d): %s\n', errno, strerror(errno));
return -1;
}
// END
bzero(buff, BUFFSIZE);
// 對(duì)應(yīng)偽代碼中的recv(connfd, buff);
recv(connfd, buff, BUFFSIZE - 1, 0);
// END
printf('Recv: %s\n', buff);
// 對(duì)應(yīng)偽代碼中的send(connfd, buff);
send(connfd, buff, strlen(buff), 0);
// END
// 對(duì)應(yīng)偽代碼中的close(connfd);
close(connfd);
// END
}
return 0;
}
客戶端搭建
客戶端相對(duì)于服務(wù)端來說會(huì)簡單一些。它需要做的事情有:
其收發(fā)數(shù)據(jù)也是借助自身的套接字來完成的。
轉(zhuǎn)換為偽代碼如下:
sockfd = socket(); // 創(chuàng)建一個(gè)socket,賦給sockfd
connect(sockfd, ip::port和一些配置); // 使用socket向指定的ip和port發(fā)起連接
scanf('%s', buff); // 讀取用戶輸入
send(sockfd, buff); // 發(fā)送數(shù)據(jù)到服務(wù)端
recv(sockfd, buff); // 從服務(wù)端接收數(shù)據(jù)
close(sockfd); // 與服務(wù)器斷開連接
這便是socket客戶端的大致流程。詳細(xì)的C++代碼如下所示:
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <sys/types.h>
#include <sys/errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFFSIZE 2048
#define SERVER_IP '192.168.19.12' // 指定服務(wù)端的IP,記得修改為你的服務(wù)端所在的ip
#define SERVER_PORT 16555 // 指定服務(wù)端的port
int main()
{
struct sockaddr_in servaddr;
char buff[BUFFSIZE];
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
printf('Create socket error(%d): %s\n', errno, strerror(errno));
return -1;
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr));
servaddr.sin_port = htons(SERVER_PORT);
if (-1 == connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
{
printf('Connect error(%d): %s\n', errno, strerror(errno));
return -1;
}
printf('Please input: ');
scanf('%s', buff);
send(sockfd, buff, strlen(buff), 0);
bzero(buff, sizeof(buff));
recv(sockfd, buff, BUFFSIZE - 1, 0);
printf('Recv: %s\n', buff);
close(sockfd);
return 0;
}
效果演示
將服務(wù)端TrainServer.cpp
和客戶端TrainClient.cpp
分別放到機(jī)子上進(jìn)行編譯:
g++ TrainServer.cpp -o TrainServer.o
g++ TrainClient.cpp -o TrainClient.o
編譯后的文件列表如下所示:
$ ls
TrainClient.cpp TrainClient.o TrainServer.cpp TrainServer.o
接著,先啟動(dòng)服務(wù)端:
$ ./TrainServer.o
Listening...
然后,再在另一個(gè)命令行窗口上啟動(dòng)客戶端:
$ ./TrainClient.o
Please input:
隨便輸入一個(gè)字符串,例如說Re0_CppNetworkProgramming
:
$ ./TrainClient.o
Please input: Re0_CppNetworkProgramming
Recv: Re0_CppNetworkProgramming
此時(shí)服務(wù)端也收到了數(shù)據(jù)并顯示出來:
$ ./TrainServer.o
Listening...
Recv: Re0_CppNetworkProgramming
你可以在服務(wù)端啟動(dòng)的時(shí)候多次打開客戶端并向服務(wù)端發(fā)送數(shù)據(jù),服務(wù)端每當(dāng)收到請(qǐng)求都會(huì)處理并返回?cái)?shù)據(jù)。
當(dāng)且僅當(dāng)服務(wù)端下按ctrl+c
的時(shí)候會(huì)關(guān)閉服務(wù)端。
2. socket庫函數(shù)淺析
本節(jié)會(huì)先從TCP連接入手,簡單回顧一下TCP連接的過程。然后再根據(jù)上一節(jié)的代碼對(duì)這個(gè)簡單客戶端/服務(wù)器的socket通信涉及到的庫函數(shù)進(jìn)行介紹。
注意:本篇中所有函數(shù)都按工作在TCP連接的情況下,并且socket默認(rèn)為阻塞的情況下講解。
TCP連接簡介
什么是TCP協(xié)議
在此之前,需要了解網(wǎng)絡(luò)的協(xié)議層模型。這里不使用OSI七層模型,而是直接通過網(wǎng)際網(wǎng)協(xié)議族進(jìn)行講解。
在網(wǎng)際網(wǎng)協(xié)議族中,協(xié)議層從上往下如下圖所示:
這個(gè)協(xié)議層所表示的意義為:如果A機(jī)和B機(jī)的網(wǎng)絡(luò)都是使用(或可以看作是)網(wǎng)際網(wǎng)協(xié)議族的話,那么從機(jī)子A上發(fā)送數(shù)據(jù)到機(jī)子B所經(jīng)過的路線大致為:
A的應(yīng)用層→A的傳輸層(TCP/UDP)→A的網(wǎng)絡(luò)層(IPv4,IPv6)→A的底層硬件(此時(shí)已經(jīng)轉(zhuǎn)化為物理信號(hào)了)→B的底層硬件→B的網(wǎng)絡(luò)層→B的傳輸層→B的應(yīng)用層
而我們?cè)谑褂胹ocket(也就是套接字)編程的時(shí)候,其實(shí)際上便是工作于應(yīng)用層和傳輸層之間,此時(shí)我們可以屏蔽掉底層細(xì)節(jié),將網(wǎng)絡(luò)傳輸簡化為:
A的應(yīng)用層→A的傳輸層→B的傳輸層→B的應(yīng)用層
而如果使用的是TCP連接的socket連接的話,每個(gè)數(shù)據(jù)包的發(fā)送的過程大致為:
數(shù)據(jù)通過socket套接字構(gòu)造符合TCP協(xié)議的數(shù)據(jù)包
在屏蔽底層協(xié)議的情況下,可以理解為TCP層直接將該數(shù)據(jù)包發(fā)往目標(biāo)機(jī)器的TCP層
目標(biāo)機(jī)器解包得到數(shù)據(jù)
其實(shí)不單是TCP,其他協(xié)議的單個(gè)數(shù)據(jù)發(fā)送過程大致也是如此。
TCP協(xié)議和與其處在同一層的UDP協(xié)議的區(qū)別主要在于其對(duì)于連接和應(yīng)用層數(shù)據(jù)的處理和發(fā)送方式。
如上一節(jié)所述,要使用TCP連接收發(fā)數(shù)據(jù)需要做三件事:
建立連接
收發(fā)數(shù)據(jù)
斷開連接
下面將對(duì)這三點(diǎn)展開說明:
建立連接:TCP三次握手
在沒進(jìn)行連接的情況下,客戶端的TCP狀態(tài)處于CLOSED
狀態(tài),服務(wù)端的TCP處于CLOSED
(未開啟監(jiān)聽)或者LISTEN
(開啟監(jiān)聽)狀態(tài)。
TCP中,服務(wù)端與客戶端建立連接的過程如下:
客戶端主動(dòng)發(fā)起連接(在socket編程中則為調(diào)用connect
函數(shù)),此時(shí)客戶端向服務(wù)端發(fā)送一個(gè)SYN包
這個(gè)SYN包可以看作是一個(gè)小數(shù)據(jù)包,不過其中沒有任何實(shí)際數(shù)據(jù),僅有諸如TCP首部和TCP選項(xiàng)等協(xié)議包必須數(shù)據(jù)??梢钥醋魇强蛻舳私o服務(wù)端發(fā)送的一個(gè)信號(hào)
此時(shí)客戶端狀態(tài)從CLOSED
切換為SYN_SENT
服務(wù)端收到SYN包,并返回一個(gè)針對(duì)該SYN包的響應(yīng)包(ACK包)和一個(gè)新的SYN包。
在socket編程中,服務(wù)端能收到SYN包的前提是,服務(wù)端已經(jīng)調(diào)用過listen
函數(shù)使其處于監(jiān)聽狀態(tài)(也就是說,其必須處于LISTEN
狀態(tài)),并且處于accept
函數(shù)等待連接的阻塞狀態(tài)。
此時(shí)服務(wù)端狀態(tài)從LISTEN
切換為SYN_RCVD
客戶端收到服務(wù)端發(fā)來的兩個(gè)包,并返回針對(duì)新的SYN包的ACK包。
此時(shí)客戶端狀態(tài)從SYN_SENT
切換至ESTABLISHED
,該狀態(tài)表示可以傳輸數(shù)據(jù)了。
服務(wù)端收到ACK包,成功建立連接,accept
函數(shù)返回出客戶端套接字。
此時(shí)服務(wù)端狀態(tài)從SYN_RCVD
切換至ESTABLISHED
收發(fā)數(shù)據(jù)
當(dāng)連接建立之后,就可以通過客戶端套接字進(jìn)行收發(fā)數(shù)據(jù)了。
斷開連接:TCP四次揮手
在收發(fā)數(shù)據(jù)之后,如果需要斷開連接,則斷開連接的過程如下:
雙方中有一方(假設(shè)為A,另一方為B)主動(dòng)關(guān)閉連接(調(diào)用close
,或者其進(jìn)程本身被終止等情況),則其向B發(fā)送FIN包
此時(shí)A從ESTABLISHED
狀態(tài)切換為FIN_WAIT_1
狀態(tài)
B接收到FIN包,并發(fā)送ACK包
此時(shí)B從ESTABLISHED
狀態(tài)切換為CLOSE_WAIT
狀態(tài)
A接收到ACK包
此時(shí)A從FIN_WAIT_1
狀態(tài)切換為FIN_WAIT_2
狀態(tài)
一段時(shí)間后,B調(diào)用自身的close
函數(shù),發(fā)送FIN包
此時(shí)B從CLOSE_WAIT
狀態(tài)切換為LAST_ACK
狀態(tài)
A接收到FIN包,并發(fā)送ACK包
此時(shí)A從FIN_WAIT_2
狀態(tài)切換為TIME_WAIT
狀態(tài)
B接收到ACK包,關(guān)閉連接
此時(shí)B從LAST_ACK
狀態(tài)切換為CLOSED
狀態(tài)
A等待一段時(shí)間(兩倍的最長生命周期)后,關(guān)閉連接
此時(shí)A從TIME_WAIT
狀態(tài)切換為CLOSED
狀態(tài)
socket函數(shù)
根據(jù)上節(jié)可以知道,socket函數(shù)用于創(chuàng)建套接字。其實(shí)更嚴(yán)謹(jǐn)?shù)闹v是創(chuàng)建一個(gè)套接字描述符(以下簡稱sockfd)。
套接字描述符本質(zhì)上類似于文件描述符,文件通過文件描述符供程序進(jìn)行讀寫,而套接字描述符本質(zhì)上也是提供給程序可以對(duì)其緩存區(qū)進(jìn)行讀寫,程序在其寫緩存區(qū)寫入數(shù)據(jù),寫緩存區(qū)的數(shù)據(jù)通過網(wǎng)絡(luò)通信發(fā)送至另一端的相同套接字的讀緩存區(qū),另一端的程序使用相同的套接字在其讀緩存區(qū)上讀取數(shù)據(jù),這樣便完成了一次網(wǎng)絡(luò)數(shù)據(jù)傳輸。
而socket
函數(shù)的參數(shù)便是用于設(shè)置這個(gè)套接字描述符的屬性。
該函數(shù)的原型如下:
#include <sys/socket.h>
int socket(int family, int type, int protocol);
family參數(shù)
該參數(shù)指明要?jiǎng)?chuàng)建的sockfd的協(xié)議族,一般比較常用的有兩個(gè):
AF_INET
:IPv4協(xié)議族
AF_INET6
:IPv6協(xié)議族
type參數(shù)
該參數(shù)用于指明套接字類型,具體有:
SOCK_STREAM
:字節(jié)流套接字,適用于TCP或SCTP協(xié)議
SOCK_DGRAM
:數(shù)據(jù)報(bào)套接字,適用于UDP協(xié)議
SOCK_SEQPACKET
:有序分組套接字,適用于SCTP協(xié)議
SOCK_RAW
:原始套接字,適用于繞過傳輸層直接與網(wǎng)絡(luò)層協(xié)議(IPv4/IPv6)通信
protocol參數(shù)
該參數(shù)用于指定協(xié)議類型。
如果是TCP協(xié)議的話就填寫IPPROTO_TCP
,UDP和SCTP協(xié)議類似。
也可以直接填寫0,這樣的話則會(huì)默認(rèn)使用family
參數(shù)和type
參數(shù)組合制定的默認(rèn)協(xié)議
(參照上面type參數(shù)的適用協(xié)議)
返回值
socket
函數(shù)在成功時(shí)會(huì)返回套接字描述符,失敗則返回-1。
失敗的時(shí)候可以通過輸出errno
來詳細(xì)查看具體錯(cuò)誤類型。
關(guān)于errno
通常一個(gè)內(nèi)核函數(shù)運(yùn)行出錯(cuò)的時(shí)候,它會(huì)定義全局變量errno
并賦值。
當(dāng)我們引入errno.h
頭文件時(shí)便可以使用這個(gè)變量。并利用這個(gè)變量查看具體出錯(cuò)原因。
一共有兩種查看的方法:
bind函數(shù)
根據(jù)上節(jié)可以知道,bind函數(shù)用于將套接字與一個(gè)ip::port
綁定。或者更應(yīng)該說是把一個(gè)本地協(xié)議地址賦予一個(gè)套接字。
該函數(shù)的原型如下:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
這個(gè)函數(shù)的參數(shù)表比較簡單:第一個(gè)是套接字描述符,第二個(gè)是套接字地址結(jié)構(gòu)體,第三個(gè)是套接字地址結(jié)構(gòu)體的長度。其含義就是將第二個(gè)的套接字地址結(jié)構(gòu)體賦給第一個(gè)的套接字描述符所指的套接字。
接下來著重講一下套接字地址結(jié)構(gòu)體
套接字地址結(jié)構(gòu)體
在bind函數(shù)的參數(shù)表中出現(xiàn)了一個(gè)名為sockaddr
的結(jié)構(gòu)體,這個(gè)便是用于存儲(chǔ)將要賦給套接字的地址結(jié)構(gòu)的通用套接字地址結(jié)構(gòu)。其定義如下:
#include <sys/socket.h>
struct sockaddr
{
uint8_t sa_len;
sa_family_t sa_family; // 地址協(xié)議族
char sa_data[14]; // 地址數(shù)據(jù)
};
當(dāng)然,我們一般不會(huì)直接使用這個(gè)結(jié)構(gòu)來定義套接字地址結(jié)構(gòu)體,而是使用更加特定化的IPv4套接字地址結(jié)構(gòu)體或IPv6套接字地址結(jié)構(gòu)體。這里只講前者。
IPv4套接字地址結(jié)構(gòu)體的定義如下:
#include <netinet/in.h>
struct in_addr
{
in_addr_t s_addr; // 32位IPv4地址
};
struct sockaddr_in
{
uint8_t sin_len; // 結(jié)構(gòu)長度,非必需
sa_family_t sin_family; // 地址族,一般為AF_****格式,常用的是AF_INET
in_port_t sin_port; // 16位TCP或UDP端口號(hào)
struct in_addr sin_addr; // 32位IPv4地址
char sin_zero[8]; // 保留數(shù)據(jù)段,一般置零
};
值得注意的是,一般而言一個(gè)sockaddr_in
結(jié)構(gòu)對(duì)我們來說有用的字段就三個(gè):
sin_family
sin_addr
sin_port
可以看到在第一節(jié)的代碼中也是只賦值了這三個(gè)成員:
#define DEFAULT_PORT 16555
// ...
struct sockaddr_in servaddr; // 定義一個(gè)IPv4套接字地址結(jié)構(gòu)體
// ...
bzero(&servaddr, sizeof(servaddr)); // 將該結(jié)構(gòu)體的所有數(shù)據(jù)置零
servaddr.sin_family = AF_INET; // 指定其協(xié)議族為IPv4協(xié)議族
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定IP地址為通配地址
servaddr.sin_port = htons(DEFAULT_PORT); // 指定端口號(hào)為16555
// 調(diào)用bind,注意第二個(gè)參數(shù)使用了類型轉(zhuǎn)換,第三個(gè)參數(shù)直接取其sizeof即可
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
{
printf('Bind error(%d): %s\n', errno, strerror(errno));
return -1;
}
其中有三個(gè)細(xì)節(jié)需要注意:
在指定IP地址的時(shí)候,一般就是使用像上面那樣的方法指定為通配地址,此時(shí)就交由內(nèi)核選擇IP地址綁定。指定特定IP的操作在講connect函數(shù)的時(shí)候會(huì)提到。
在指定端口的時(shí)候,可以直接指定端口號(hào)為0,此時(shí)表示端口號(hào)交由內(nèi)核選擇(也就是進(jìn)程不指定端口號(hào))。但一般而言對(duì)于服務(wù)器來說,不指定端口號(hào)的情況是很罕見的,因?yàn)榉?wù)器一般都需要暴露一個(gè)端口用于讓客戶端知道并作為連接的參數(shù)。
注意到不管是賦值IP還是端口,都不是直接賦值,而是使用了類似htons()
或htonl()
的函數(shù),這便是字節(jié)排序函數(shù)。
字節(jié)排序函數(shù)
首先,不同的機(jī)子上對(duì)于多字節(jié)變量的字節(jié)存儲(chǔ)順序是不同的,有大端字節(jié)序和小端字節(jié)序兩種。
那這就意味著,將機(jī)子A的變量原封不動(dòng)傳到機(jī)子B上,其值可能會(huì)發(fā)生變化(本質(zhì)上數(shù)據(jù)沒有變化,但如果兩個(gè)機(jī)子的字節(jié)序不一樣的話,解析出來的值便是不一樣的)。這顯然是不好的。
故我們需要引入一個(gè)通用的規(guī)范,稱為網(wǎng)絡(luò)字節(jié)序。引入網(wǎng)絡(luò)字節(jié)序之后的傳遞規(guī)則就變?yōu)椋?/p>
機(jī)子A先將變量由自身的字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序
發(fā)送轉(zhuǎn)換后的數(shù)據(jù)
機(jī)子B接到轉(zhuǎn)換后的數(shù)據(jù)之后,再將其由網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為自己的字節(jié)序
其實(shí)就是很常規(guī)的統(tǒng)一標(biāo)準(zhǔn)中間件的做法。
在Linux中,位于<netinet/in.h>
中有四個(gè)用于主機(jī)字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間相互轉(zhuǎn)換的函數(shù):
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue); //host to network, 16bit
uint32_t htonl(uint32_t host32bitvalue); //host to network, 32bit
uint16_t ntohs(uint16_t net16bitvalue); //network to host, 16bit
uint32_t ntohl(uint32_t net32bitvalue); //network to host, 32bit
返回值
若成功則返回0,否則返回-1并置相應(yīng)的errno
。
比較常見的錯(cuò)誤是錯(cuò)誤碼EADDRINUSE
('Address already in use',地址已使用)。
listen函數(shù)
listen函數(shù)的作用就是開啟套接字的監(jiān)聽狀態(tài),也就是將套接字從CLOSE
狀態(tài)轉(zhuǎn)換為LISTEN
狀態(tài)。
該函數(shù)的原型如下:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
其中,sockfd
為要設(shè)置的套接字,backlog
為服務(wù)器處于LISTEN
狀態(tài)下維護(hù)的隊(duì)列長度和的最大值。
關(guān)于backlog
這是一個(gè)可調(diào)參數(shù)。
其意義為,服務(wù)器套接字處于LISTEN
狀態(tài)下所維護(hù)的未完成連接隊(duì)列(SYN隊(duì)列)和已完成連接隊(duì)列(Accept隊(duì)列)的長度和的最大值。
↑ 這個(gè)是原本的意義,現(xiàn)在的backlog
僅指Accept隊(duì)列的最大長度,SYN隊(duì)列的最大長度由系統(tǒng)的另一個(gè)變量決定。
這兩個(gè)隊(duì)列用于維護(hù)與客戶端的連接,其中:
客戶端發(fā)送的SYN到達(dá)服務(wù)器之后,服務(wù)端返回SYN/ACK,并將該客戶端放置SYN隊(duì)列中(第一次+第二次握手)
當(dāng)服務(wù)端接收到客戶端的ACK之后,完成握手,服務(wù)端將對(duì)應(yīng)的連接從SYN隊(duì)列中取出,放入Accept隊(duì)列,等待服務(wù)器中的accept接收并處理其請(qǐng)求(第三次握手)
backlog調(diào)參
backlog
是由程序員決定的,不過最后的隊(duì)列長度其實(shí)是min(backlog, /proc/sys/net/core/somaxconn , net.ipv4.tcp_max_syn_backlog )
,后者直接讀取對(duì)應(yīng)位置文件就有了。
不過由于后者是可以修改的,故這里討論的backlog
實(shí)際上是這兩個(gè)值的最小值。
至于如何調(diào)參,可以參考這篇博客:
https://ylgrgyq./2017/05/18/tcp-backlog/
事實(shí)上backlog
僅僅是與Accept隊(duì)列的最大長度相關(guān)的參數(shù),實(shí)際的隊(duì)列最大長度視不同的操作系統(tǒng)而定。例如說MacOS上使用傳統(tǒng)的Berkeley算法基于backlog
參數(shù)進(jìn)行計(jì)算,而Linux2.4.7上則是直接等于backlog+3
。
返回值
若成功則返回0,否則返回-1并置相應(yīng)的errno
。
connect函數(shù)
該函數(shù)用于客戶端跟綁定了指定的ip和port并且處于LISTEN
狀態(tài)的服務(wù)端進(jìn)行連接。
在調(diào)用connect函數(shù)的時(shí)候,調(diào)用方(也就是客戶端)便會(huì)主動(dòng)發(fā)起TCP三次握手。
該函數(shù)的原型如下:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
其中第一個(gè)參數(shù)為客戶端套接字,第二個(gè)參數(shù)為用于指定服務(wù)端的ip和port的套接字地址結(jié)構(gòu)體,第三個(gè)參數(shù)為該結(jié)構(gòu)體的長度。
操作上比較類似于服務(wù)端使用bind函數(shù)(雖然做的事情完全不一樣),唯一的區(qū)別在于指定ip這塊。服務(wù)端調(diào)用bind函數(shù)的時(shí)候無需指定ip,但客戶端調(diào)用connect函數(shù)的時(shí)候則需要指定服務(wù)端的ip。
在客戶端的代碼中,令套接字地址結(jié)構(gòu)體指定ip的代碼如下:
inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
這個(gè)就涉及到ip地址的表達(dá)格式與數(shù)值格式相互轉(zhuǎn)換的函數(shù)。
IP地址格式轉(zhuǎn)換函數(shù)
IP地址一共有兩種格式:
顯然,當(dāng)我們需要將一個(gè)IP賦進(jìn)套接字地址結(jié)構(gòu)體中,就需要將其轉(zhuǎn)換為數(shù)值格式。
在<arpa/inet.h>
中提供了兩個(gè)函數(shù)用于IP地址格式的相互轉(zhuǎn)換:
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
其中:
inet_pton()
函數(shù)用于將IP地址從表達(dá)格式轉(zhuǎn)換為數(shù)值格式
第一個(gè)參數(shù)指定協(xié)議族(AF_INET
或AF_INET6
)
第二個(gè)參數(shù)指定要轉(zhuǎn)換的表達(dá)格式的IP地址
第三個(gè)參數(shù)指定用于存儲(chǔ)轉(zhuǎn)換結(jié)果的指針
對(duì)于返回結(jié)果而言:
若轉(zhuǎn)換成功則返回1
若表達(dá)格式的IP地址格式有誤則返回0
若出錯(cuò)則返回-1
inet_ntop()
函數(shù)用于將IP地址從數(shù)值格式轉(zhuǎn)換為表達(dá)格式
第一個(gè)參數(shù)指定協(xié)議族
第二個(gè)參數(shù)指定要轉(zhuǎn)換的數(shù)值格式的IP地址
第三個(gè)參數(shù)指定用于存儲(chǔ)轉(zhuǎn)換結(jié)果的指針
第四個(gè)參數(shù)指定第三個(gè)參數(shù)指向的空間的大小,用于防止緩存區(qū)溢出
對(duì)于返回結(jié)果而言
返回值
若成功則返回0,否則返回-1并置相應(yīng)的errno
。
其中connect函數(shù)會(huì)出錯(cuò)的幾種情況:
若客戶端在發(fā)送SYN包之后長時(shí)間沒有收到響應(yīng),則返回ETIMEOUT
錯(cuò)誤
若客戶端在發(fā)送SYN包之后收到的是RST包的話,則會(huì)立刻返回ECONNREFUSED
錯(cuò)誤
若客戶端在發(fā)送SYN包的時(shí)候在中間的某一臺(tái)路由器上發(fā)生ICMP錯(cuò)誤,則會(huì)發(fā)生EHOSTUNREACH
或ENETUNREACH
錯(cuò)誤
事實(shí)上跟處理未響應(yīng)一樣,為了排除偶然因素,客戶端遇到這個(gè)問題的時(shí)候會(huì)保存內(nèi)核信息,隔一段時(shí)間之后再重發(fā)SYN包,在多次發(fā)送失敗之后才會(huì)報(bào)錯(cuò)
路由器發(fā)生ICMP錯(cuò)誤的原因是,路由器上根據(jù)目標(biāo)IP查找轉(zhuǎn)發(fā)表但查不到針對(duì)目標(biāo)IP應(yīng)該如何轉(zhuǎn)發(fā),則會(huì)發(fā)生ICMP錯(cuò)誤
可能的原因是目標(biāo)服務(wù)端的IP地址不可達(dá),或者路由器配置錯(cuò)誤,也有可能是因?yàn)殡姴ǜ蓴_等隨機(jī)因素導(dǎo)致數(shù)據(jù)包錯(cuò)誤,進(jìn)而導(dǎo)致路由無法轉(zhuǎn)發(fā)
由于connect函數(shù)在發(fā)送SYN包之后就會(huì)將自身的套接字從CLOSED
狀態(tài)置為SYN_SENT
狀態(tài),故當(dāng)connect報(bào)錯(cuò)之后需要主動(dòng)將套接字狀態(tài)置回CLOSED
。此時(shí)需要通過調(diào)用close函數(shù)主動(dòng)關(guān)閉套接字實(shí)現(xiàn)。
故原版的客戶端代碼需要做一個(gè)修改:
if (-1 == connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
{
printf('Connect error(%d): %s\n', errno, strerror(errno));
close(sockfd); // 新增代碼,當(dāng)connect出錯(cuò)時(shí)需要關(guān)閉套接字
return -1;
}
accept函數(shù)
根據(jù)上一節(jié)所述,該函數(shù)用于跟客戶端建立連接,并返回客戶端套接字。
更準(zhǔn)確的說,accept函數(shù)由TCP服務(wù)器調(diào)用,用于從Accept隊(duì)列中pop出一個(gè)已完成的連接。若Accept隊(duì)列為空,則accept函數(shù)所在的進(jìn)程阻塞。
該函數(shù)的原型如下:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
其中第一個(gè)參數(shù)為服務(wù)端自身的套接字,第二個(gè)參數(shù)用于接收客戶端的套接字地址結(jié)構(gòu)體,第三個(gè)參數(shù)用于接收第二個(gè)參數(shù)的結(jié)構(gòu)體的長度。
返回值
當(dāng)accept函數(shù)成功拿到一個(gè)已完成連接時(shí),其會(huì)返回該連接對(duì)應(yīng)的客戶端套接字描述符,用于后續(xù)的數(shù)據(jù)傳輸。
若發(fā)生錯(cuò)誤則返回-1并置相應(yīng)的errno
。
recv函數(shù)&send函數(shù)
recv函數(shù)用于通過套接字接收數(shù)據(jù),send函數(shù)用于通過套接字發(fā)送數(shù)據(jù)
這兩個(gè)函數(shù)的原型如下:
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
其中:
第一個(gè)參數(shù)為要讀寫的套接字
第二個(gè)參數(shù)指定要接收數(shù)據(jù)的空間的指針(recv)或要發(fā)送的數(shù)據(jù)(send)
第三個(gè)參數(shù)指定最大讀取的字節(jié)數(shù)(recv)或發(fā)送的數(shù)據(jù)的大?。╯end)
第四個(gè)參數(shù)用于設(shè)置一些參數(shù),默認(rèn)為0
目前用不到第四個(gè)參數(shù),故暫時(shí)不做展開
事實(shí)上,去掉第四個(gè)參數(shù)的情況下,recv跟read函數(shù)類似,send跟write函數(shù)類似。這兩個(gè)函數(shù)的本質(zhì)也是一種通過描述符進(jìn)行的IO,只是在這里的描述符為套接字描述符。
返回值
在recv函數(shù)中:
在send函數(shù)中:
close函數(shù)
根據(jù)第一節(jié)所述,該函數(shù)用于斷開連接?;蛘吒唧w的講,該函數(shù)用于關(guān)閉套接字,并終止TCP連接。
該函數(shù)的原型如下:
#include <unistd.h>
int close(int sockfd);
返回值
同樣的,若close成功則返回0,否則返回-1并置errno
。
常見的錯(cuò)誤為關(guān)閉一個(gè)無效的套接字。
3. 搭建HTTP服務(wù)器
本節(jié)將會(huì)將最開始的簡單服務(wù)器改為可以接收并處理HTTP請(qǐng)求的HTTP服務(wù)器。
在改裝之前,首先需要明白HTTP服務(wù)器能做什么。
所謂HTTP服務(wù)器,通俗點(diǎn)說就是可以使用像http://192.168.19.12:16555/
這樣的URL進(jìn)行服務(wù)器請(qǐng)求,并且能得到一個(gè)合法的返回。
其實(shí)之前搭的服務(wù)器已經(jīng)可以處理這種HTTP請(qǐng)求了,只是請(qǐng)求的返回不合法罷了(畢竟只是把發(fā)送的數(shù)據(jù)再回傳一遍)。在這里可以做個(gè)試驗(yàn),看看現(xiàn)階段的服務(wù)器是如何處理HTTP請(qǐng)求的:
首先,開啟服務(wù)器:
$ ./TrainServer.o
Listening...
之后,另開一個(gè)命令行,使用curl
指令發(fā)送一個(gè)HTTP請(qǐng)求(其實(shí)就是類似瀏覽器打開http://192.168.19.12:16555/
的頁面一樣):
$ curl -v 'http://192.168.19.12:16555/'
* About to connect() to 192.168.19.12 port 16555 (#0)
* Trying 192.168.19.12... connected
* Connected to 192.168.19.12 (192.168.19.12) port 16555 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
> Host: 192.168.19.12:16555
> Accept: */*
>
GET / HTTP/1.1
User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Host: 192.168.19.12:16555
Accept: */*
* Connection #0 to host 192.168.19.12 left intact
* Closing connection #0
其中:
GET / HTTP/1.1
User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Host: 192.168.19.12:16555
Accept: */*
便是接收到的返回?cái)?shù)據(jù),我們可以通過服務(wù)器自己輸出的日志確認(rèn)這一點(diǎn):
$ ./TrainServer.o
Listening...
Recv: GET / HTTP/1.1
User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Host: 192.168.19.12:16555
Accept: */*
(注意其中的Recv:
是程序自己的輸出)
可以看到,當(dāng)我們通過http://192.168.19.12:16555/
訪問服務(wù)器的時(shí)候,其實(shí)就相當(dāng)于發(fā)這一長串東西給服務(wù)器。
事實(shí)上這一串東西就是HTTP請(qǐng)求串,其格式如下:
方法名 URL 協(xié)議版本 //請(qǐng)求行
字段名:字段值 //消息報(bào)頭
字段名:字段值 //消息報(bào)頭
...
字段名:字段值 //消息報(bào)頭
請(qǐng)求正文 //可選
每一行都以\r\n
結(jié)尾,表示一個(gè)換行。
于是對(duì)應(yīng)的就有一個(gè)叫做HTTP返回串的東西,這個(gè)也是有格式規(guī)定的:
協(xié)議版本 狀態(tài)碼 狀態(tài)描述 //狀態(tài)行
字段名:字段值 //消息報(bào)頭
字段名:字段值 //消息報(bào)頭
...
字段名:字段值 //消息報(bào)頭
響應(yīng)正文 //可選
其中,狀態(tài)碼有如下的幾種:
1xx:指示信息,表示請(qǐng)求已接收,繼續(xù)處理
2xx:成功,表示請(qǐng)求已被成功接收、理解、接受
3xx:重定向,要完成請(qǐng)求必須進(jìn)行更進(jìn)一步的操作
4xx:客戶端錯(cuò)誤,請(qǐng)求有語法錯(cuò)誤或請(qǐng)求無法實(shí)現(xiàn)
5xx:服務(wù)器端錯(cuò)誤,服務(wù)器未能實(shí)現(xiàn)合法的請(qǐng)求
比較常見的就有200(OK),404(Not Found),502(Bad Gateway)。
顯然我們需要返回一個(gè)成功的HTTP返回串,故這里就需要使用200,于是第一行就可以是:
HTTP/1.1 200 OK
至于字段名及其對(duì)應(yīng)的字段值則按需加就行了,具體的可以上網(wǎng)查有哪些選項(xiàng)。
這里為了簡潔就只加一個(gè)就行了:
Connection: close
這個(gè)表示該連接為短連接,換句話說就是傳輸一個(gè)來回之后就關(guān)閉連接。
最后,正文可以隨便寫點(diǎn)上面,例如Hello
什么的。于是完成的合法返回串就搞定了:
HTTP/1.1 200 OK
Connection: close
Hello
在代碼中,我們可以寫一個(gè)函數(shù)用于在buff中寫入這個(gè)返回串:
void setResponse(char *buff)
{
bzero(buff, sizeof(buff));
strcat(buff, 'HTTP/1.1 200 OK\r\n');
strcat(buff, 'Connection: close\r\n');
strcat(buff, '\r\n');
strcat(buff, 'Hello\n');
}
然后在main()
中的recv()
之后,send()
之前調(diào)用該函數(shù)就可以了。
setResponse(buff);
接著把更新好的HTTP服務(wù)器放到機(jī)子上運(yùn)行,再使用curl
試一遍:
$ curl -v 'http://192.168.19.12:16555/'
* About to connect() to 192.168.19.12 port 16555 (#0)
* Trying 192.168.19.12... connected
* Connected to 192.168.19.12 (192.168.19.12) port 16555 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
> Host: 192.168.19.12:16555
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: close
<
Hello
* Closing connection #0
可以得到正確的返回串頭和正文了。
于是,一個(gè)簡單的HTTP服務(wù)器便搭好了,它的功能是,只要訪問該服務(wù)器就會(huì)返回Hello
。
4. 壓力測(cè)試入門
由于在不同機(jī)器上進(jìn)行壓力測(cè)試的結(jié)果不同,故將本次及之后的實(shí)驗(yàn)機(jī)器的配置貼出來,以供比對(duì):
CPU:4核64位 Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
內(nèi)存:10GB
操作系統(tǒng):Tencent tlinux release 1.2 (Final)
介紹了這么多,我們一直都只關(guān)注服務(wù)器能不能跑,卻沒有關(guān)注過服務(wù)器能力強(qiáng)不強(qiáng)。
怎樣才算強(qiáng)呢?一般而言搭一個(gè)能正確響應(yīng)請(qǐng)求的服務(wù)器是不難的,但搭建一個(gè)可以在大量請(qǐng)求下仍能正確響應(yīng)請(qǐng)求的服務(wù)器就很難了,這里的大量請(qǐng)求一般指的有:
總的請(qǐng)求數(shù)多
請(qǐng)求并發(fā)量大
于是要怎么進(jìn)行壓力測(cè)試呢?由于我們的服務(wù)器是HTTP服務(wù)器,故這個(gè)時(shí)候就可以直接使用Apache Bench壓力測(cè)試工具了。
由于這個(gè)工具的測(cè)試方式是模擬大量的HTTP請(qǐng)求,故無法適用于之前的裸socket服務(wù)器,所以只能測(cè)試現(xiàn)在的HTTP服務(wù)器。
使用方法很簡答,直接運(yùn)行以下指令即可:
ab -c 1 -n 10000 'http://192.168.19.12:16555/'
這個(gè)指令中,-c
后面跟著的數(shù)字表示請(qǐng)求并發(fā)數(shù),-n
后面跟著的數(shù)字表示總請(qǐng)求數(shù)。于是上面的指令表示的就是【并發(fā)數(shù)為1,一共10000條請(qǐng)求】,其實(shí)就是相當(dāng)于我們直接curl
10000次。
執(zhí)行之后的效果如下:
$ ab -c 1 -n 10000 'http://192.168.19.12:16555/'
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www./
Licensed to The Apache Software Foundation, http://www./
Benchmarking 192.168.19.12 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software:
Server Hostname: 192.168.19.12
Server Port: 16555
Document Path: /
Document Length: 6 bytes
Concurrency Level: 1
Time taken for tests: 3.620 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 440000 bytes
HTML transferred: 60000 bytes
Requests per second: 2762.46 [#/sec] (mean)
Time per request: 0.362 [ms] (mean)
Time per request: 0.362 [ms] (mean, across all concurrent requests)
Transfer rate: 118.70 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 0 12.1 0 670
Waiting: 0 0 12.1 0 670
Total: 0 0 12.1 0 670
Percentage of the requests served within a certain time (ms)
50% 0
66% 0
75% 0
80% 0
90% 0
95% 0
98% 0
99% 0
100% 670 (longest request)
其中比較重要的有:
Failed requests:失敗請(qǐng)求數(shù)。
Requests per second:每秒處理的請(qǐng)求數(shù),也就是吞吐率。
Transfer rate:傳輸速率,表示每秒收到多少的數(shù)據(jù)量。
最下面的表:表示百分之xx的請(qǐng)求數(shù)的響應(yīng)時(shí)間的分布,可以比較直觀的看出請(qǐng)求響應(yīng)時(shí)間分布。
在這次壓力測(cè)試中,撇開其他數(shù)據(jù)不管,至少失敗請(qǐng)求數(shù)是0,已經(jīng)算是能夠用的了(在并發(fā)數(shù)為1的情況下)。
那么,更高的請(qǐng)求量呢?例如10000并發(fā),100000請(qǐng)求數(shù)呢:
ab -c 10000 -n 100000 -r 'http://192.168.19.12:16555/'
這里加上-r
是為了讓其在出錯(cuò)的時(shí)候也繼續(xù)壓測(cè)(這么大數(shù)據(jù)量肯定會(huì)有請(qǐng)求錯(cuò)誤的)
結(jié)果如下(省略部分輸出,用...
表示省略的輸出):
$ ab -c 10000 -n 100000 -r 'http://192.168.19.12:16555/'
...
Complete requests: 100000
Failed requests: 34035
(Connect: 0, Receive: 11345, Length: 11345, Exceptions: 11345)
Write errors: 0
Total transferred: 4133096 bytes
HTML transferred: 563604 bytes
Requests per second: 3278.15 [#/sec] (mean)
Time per request: 3050.501 [ms] (mean)
Time per request: 0.305 [ms] (mean, across all concurrent requests)
Transfer rate: 132.31 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 481 1061.9 146 7392
Processing: 31 1730 3976.7 561 15361
Waiting: 0 476 319.3 468 10064
Total: 175 2210 3992.2 781 15361
Percentage of the requests served within a certain time (ms)
50% 781
66% 873
75% 1166
80% 1783
90% 4747
95% 15038
98% 15076
99% 15087
100% 15361 (longest request)
可以看出,這個(gè)時(shí)候的失敗請(qǐng)求數(shù)已經(jīng)飆到一個(gè)難以忍受的地步了(34%
的失敗率啊。。),而且請(qǐng)求響應(yīng)時(shí)長也十分的長(甚至有到15秒的),這顯然已經(jīng)足夠證明在這種并發(fā)量和請(qǐng)求數(shù)的情況下,我們的服務(wù)器宛如一個(gè)土豆。
一個(gè)優(yōu)化的Tip
那么在當(dāng)前階段下要怎么優(yōu)化這個(gè)服務(wù)器呢?注意到服務(wù)器端在每接收到一個(gè)請(qǐng)求的時(shí)候都會(huì)將收到的內(nèi)容在屏幕上打印出來。要知道這種與輸出設(shè)備交互的IO是很慢的,于是這便是一個(gè)要優(yōu)化掉的點(diǎn)。
考慮到日志是必須的(雖然這僅僅是將收到的內(nèi)容打印出來,不算嚴(yán)格意義上的日志),我們不能直接去掉日志打印,故我們可以嘗試將日志打印轉(zhuǎn)為文件輸出。
首先,先寫一個(gè)用于在文件中打日志的類:
#define LOG_BUFFSIZE 65536
class Logger
{
char buff[LOG_BUFFSIZE];
int buffLen;
FILE *fp;
public:
Logger()
{
bzero(buff, sizeof(buff));
buffLen = 0;
fp = fopen('TrainServer.log', 'a');
}
void Flush()
{
fputs(buff, fp);
bzero(buff, sizeof(buff));
buffLen = 0;
}
void Log(const char *str, int len)
{
if (buffLen + len > LOG_BUFFSIZE - 10)
{
Flush();
}
for (int i = 0; i < len; i++)
{
buff[buffLen] = str[i];
buffLen++;
}
}
~Logger()
{
if (buffLen != 0)
{
Flush();
}
fclose(fp);
}
}logger;
這里使用了一個(gè)長的字符串作為日志緩沖區(qū),每次寫日志的時(shí)候往日志緩沖區(qū)中寫,直到緩沖區(qū)快滿了或者進(jìn)程終止的時(shí)候才把緩沖區(qū)的內(nèi)容一次性寫入文件中。這樣便能減少文件讀寫次數(shù)。
那么在打日志的位置便可以直接調(diào)用Log()
方法:
// 替換掉printf('Recv: %s\n', buff);
logger.Log('Recv: ', 6);
logger.Log(buff, strlen(buff));
接著我們將服務(wù)器部署上去,然后用ab
指令發(fā)送一個(gè)請(qǐng)求(并發(fā)數(shù)1,請(qǐng)求總數(shù)1),可以看到目錄下就生成了日志文件:
$ ls
TrainClient.cpp TrainClient.o TrainServer.cpp TrainServer.log TrainServer.o
打開日志可以看到這個(gè)內(nèi)容跟之前的屏幕輸出一致。統(tǒng)計(jì)行數(shù)可以得到單次成功的請(qǐng)求所記錄的日志一共有5行:
$ cat TrainServer.log
Recv: GET / HTTP/1.0
Host: 192.168.19.12:16555
User-Agent: ApacheBench/2.3
Accept: */*
$ cat TrainServer.log | wc -l
5
接著我們測(cè)試一下在一定規(guī)模的數(shù)據(jù)下日志是否能正常工作。這個(gè)時(shí)候?qū)⒄?qǐng)求量加大:
ab -c 100 -n 1000 'http://192.168.19.12:16555/'
結(jié)果如下(省略部分輸出,用...
表示省略的輸出):
$ ab -c 1 -n 10000 'http://192.168.19.12:16555/'
...
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 440000 bytes
HTML transferred: 60000 bytes
Requests per second: 15633.89 [#/sec] (mean)
Time per request: 0.064 [ms] (mean)
Time per request: 0.064 [ms] (mean, across all concurrent requests)
Transfer rate: 671.77 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 0 0 0.0 0 0
Waiting: 0 0 0.0 0 0
Total: 0 0 0.0 0 0
Percentage of the requests served within a certain time (ms)
50% 0
66% 0
75% 0
80% 0
90% 0
95% 0
98% 0
99% 0
100% 0 (longest request)
可以看到這10000次請(qǐng)求沒有失敗請(qǐng)求,故如果日志正確記錄的話應(yīng)該會(huì)有50000行。
于是我們查看一下日志行數(shù):
$ cat TrainServer.log | wc -l
50000
一切正常。必要的話還可以用cat
或者head
隨機(jī)檢查日志內(nèi)容。
接著就可以試一下改良后的服務(wù)器的性能了,還是一萬并發(fā)十萬請(qǐng)求:
$ ab -c 10000 -n 100000 -r 'http://192.168.19.12:16555/'
...
Complete requests: 100000
Failed requests: 1164
(Connect: 0, Receive: 388, Length: 388, Exceptions: 388)
Write errors: 0
Total transferred: 4471368 bytes
HTML transferred: 609732 bytes
Requests per second: 5503.42 [#/sec] (mean)
Time per request: 1817.053 [ms] (mean)
Time per request: 0.182 [ms] (mean, across all concurrent requests)
Transfer rate: 240.31 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1149 1572.6 397 7430
Processing: 36 362 972.8 311 15595
Waiting: 0 229 250.7 217 15427
Total: 193 1511 1845.6 780 16740
Percentage of the requests served within a certain time (ms)
50% 780
66% 1476
75% 1710
80% 1797
90% 3695
95% 3825
98% 7660
99% 7817
100% 16740 (longest request)
與優(yōu)化前的服務(wù)器性能對(duì)比如下: