乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      從零開始的C 網(wǎng)絡(luò)編程

       西北望msm66g9f 2019-10-22

      導(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ù)端和客戶端通信需要做三件事:

      • 服務(wù)端與客戶端進(jìn)行連接

      • 服務(wù)端與客戶端之間傳輸數(shù)據(jù)

      • 服務(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í)就是:

      • 接收數(shù)據(jù):使用客戶端套接字拿到客戶端發(fā)來的數(shù)據(jù),并將其存于buff中。(偽代碼中簡稱為recv()

      • 發(fā)送數(shù)據(jù):使用客戶端套接字,將buff中的數(shù)據(jù)發(fā)回去。(偽代碼中簡稱為send()

      在收發(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ì)簡單一些。它需要做的事情有:

      • 創(chuàng)建socket

      • 使用socket和已知的服務(wù)端的ip和port連接服務(wù)端

      • 收發(fā)數(shù)據(jù)

      • 關(guān)閉連接

      其收發(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ò)原因。

      一共有兩種查看的方法:

      • 直接輸出errno,根據(jù)輸出的錯(cuò)誤碼進(jìn)行Google搜索解決方案

      • 當(dāng)然也可以直接翻man手冊(cè)

      • 借助strerror()函數(shù),使用strerror(errno)得到一個(gè)具體描述其錯(cuò)誤的字符串。一般可以通過其描述定位問題所在,實(shí)在不行也可以拿這個(gè)輸出去Google搜索解決方案


      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á)格式:也就是我們能看得懂的格式,例如'192.168.19.12'這樣的字符串

      • 數(shù)值格式:可以存入套接字地址結(jié)構(gòu)體的格式,數(shù)據(jù)類型為整型

      顯然,當(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_INETAF_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ū)溢出

        • 第四個(gè)參數(shù)可以使用預(yù)設(shè)的變量:

        • #include <netinet/in.h>

          #define INET_ADDRSTRLEN    16  // IPv4地址的表達(dá)格式的長度
          #define INET6_ADDRSTRLEN 46    // IPv6地址的表達(dá)格式的長度
      • 對(duì)于返回結(jié)果而言

        • 若轉(zhuǎn)換成功則返回指向返回結(jié)果的指針

        • 若出錯(cuò)則返回NULL

      返回值

      若成功則返回0,否則返回-1并置相應(yīng)的errno。

      其中connect函數(shù)會(huì)出錯(cuò)的幾種情況:

      • 若客戶端在發(fā)送SYN包之后長時(shí)間沒有收到響應(yīng),則返回ETIMEOUT錯(cuò)誤

        • 一般而言,如果長時(shí)間沒有收到響應(yīng),客戶端會(huì)重發(fā)SYN包,若超過一定次數(shù)重發(fā)仍沒響應(yīng)的話則會(huì)返回該錯(cuò)誤

        • 可能的原因是目標(biāo)服務(wù)端的IP地址不存在

      • 若客戶端在發(fā)送SYN包之后收到的是RST包的話,則會(huì)立刻返回ECONNREFUSED錯(cuò)誤

        • 當(dāng)客戶端的SYN包到達(dá)目標(biāo)機(jī)之后,但目標(biāo)機(jī)的對(duì)應(yīng)端口并沒有正在LISTEN的套接字,那么目標(biāo)機(jī)會(huì)發(fā)一個(gè)RST包給客戶端

        • 可能的原因是目標(biāo)服務(wù)端沒有運(yùn)行,或者沒運(yùn)行在客戶端知道的端口上

      • 若客戶端在發(fā)送SYN包的時(shí)候在中間的某一臺(tái)路由器上發(fā)生ICMP錯(cuò)誤,則會(huì)發(fā)生EHOSTUNREACHENETUNREACH錯(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ù)中:

      • 若成功,則返回所讀取到的字節(jié)數(shù)

      • 否則返回-1,置errno

      在send函數(shù)中:

      • 若成功,則返回成功寫入的字節(jié)數(shù)

      • 事實(shí)上,當(dāng)返回值與nbytes不等時(shí),也可以認(rèn)為其出錯(cuò)。

      • 否則返回-1,置errno

      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)于我們直接curl10000次。

      執(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ì)比如下:
      可以看到,相比起來整體還是優(yōu)化了不少了,尤其是失敗率,從34%下降到不到2%

      總結(jié)

      本文通過一個(gè)簡單的C++客戶端/服務(wù)器例子講述了C++網(wǎng)絡(luò)編程的基礎(chǔ)以及一些關(guān)于壓力測(cè)試的入門知識(shí)。讀者可以借此對(duì)C++網(wǎng)絡(luò)編程有一個(gè)大體的認(rèn)識(shí),也算是從零開始的C++網(wǎng)絡(luò)編程的一個(gè)入門吧。

        本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評(píng)論

        發(fā)表

        請(qǐng)遵守用戶 評(píng)論公約

        類似文章 更多