高性能Socket服務器編程-01

   要開始建造我們的高性能socket服務器大廈之前,還是讓我們先從泥水匠做起吧,先來了解以下泥沙和工具吧。

記得前面說的嗎?一次只做一件事,並且做好它。現在我們就拋開所有雜念和對高性能socket服務器的各種猜想,先做一個最基本的socket服務器端程序。

等我們逐步熟悉了泥沙和工具,我們再殺回來逐個幹掉高深莫測的服務器架構設計,這就是我們的行動計劃。

這裏先貼出本章的示例代碼,我再根據這個代碼跟大家逐步講解socket編程的關鍵知識點:

socketd.c

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h>   #define SD_PORT       10086 #define SD_BACK_LOG   10   int sd_listener_fd;   void sd_init () {     int reuse = 1;      struct sockaddr_in addr;      if ((sd_listener_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)     {         perror("Create listener socket failed");         exit(-1);     }      if (setsockopt(sd_listener_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)     {         perror("Setup listener socket failed");         exit(-1);     }      bzero(&(addr.sin_zero), 8);      addr.sin_family      = AF_INET;     addr.sin_port        = htons(SD_PORT);     addr.sin_addr.s_addr = htonl(INADDR_ANY);      if (bind(sd_listener_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1)     {         perror("Bind listener socket address failed");         exit(-1);     }      if (listen(sd_listener_fd, SD_BACK_LOG) == -1)     {         perror("Listen port failed");         exit(-1);     } }   void sd_loop () {     char buf[1024];      int ret = 0;      int client_fd;      int client_addr_len;      struct sockaddr_in client_addr;      printf("Waiting connect on port %dn", SD_PORT);      client_addr_len = sizeof(client_addr);      client_fd = accept(sd_listener_fd, (struct sockaddr *)&client_addr, &client_addr_len);      printf("Client connectedn");      for (;;)     {         if ((ret = read(client_fd, buf, 1024)) == 0)         {             close(client_fd);              printf("Client closedn");              break;         }         else         {             write(client_fd, buf, ret);         }     } }   void sd_down () {     close(sd_listener_fd);      printf("Server shutdownn"); }   int main (int argc, char *argv[]) {     sd_init();      sd_loop();      sd_down();      return 1; }

上面的代碼是一個簡單的echo服務器,它一次只處理一個連接,在客戶端退出時服務器端也跟着關閉

你可以複製上面的代碼保存爲socketd.c,然後打開終端,切換到文件所在目錄,輸入:

cc socketd.c -o socketd

不出意外的話,我們的最原始版socket服務器就編譯好了。然後輸入:

./socketd

服務器就啓動了。另外再開一個終端,輸入:

telnet localhost 10086

這時候telnet應該能連上socket服務器,你可以在telnet裏面輸入一些文字,然後回車,服務器應該會將你發送的內容原樣返回。

當你玩膩了,就在telnet界面按住Ctrl鍵,然後輸入“]“,回車。這時候telent會切換到命令界面,輸入q,回車,退出telent。

telnet退出後,相當於客戶端斷開了連接,按代碼邏輯上面的示例程序應該會跟着退出。

順便說一下,像上面示例這樣接受並原樣返回客戶端請求內容的socket服務器叫做echo服務器,名字誰取的我不知道,反正大家都這麼叫。 :)

下面我們來分析一下這段代碼,我們從大結構分析入手,再深入到每個函數的介紹。

閱讀這段代碼要從main函數開始,main函數逐步調用了三個sd_開頭的函數,sd_init() 初始化服務器 -> sd_loop() 服務器循環處理請求 -> sd_down() 服務器關閉。

sd_init 函數中的代碼是典型的服務器端socket初始化過程,socket() 創建套接字 -> bind() 綁定地址 -> listener() 開始監聽端口。

sd_loop 函數中的代碼則是一個簡單的接收客戶端請求並回發數據的示例,accept() 接受新連接 -> read() 接受請求數據 -> write()發送數據。

sd_down 函數中的代碼演示瞭如何關閉套接字,close() 就這麼簡單。

下面我以函數註釋的方式一一註釋上面代碼涉及到的系統函數,這樣可以不需要附加太多廢話的描述並條理清晰。

/*  * 功能:創建socket  * 返回:成功時,返回socket文件描述符;失敗時,返回-1,可以通過errno獲取錯誤類型  * 參數:  *      domain   - 地址種類,較常用的有AF_INET和AF_INET6,分別對應IPv4協議和IPv6協議  *      type     - 套接字類型,較常用的有SOCK_STREAM和SOCK_DGRAM,分別對應TCP/IP協議和UDP協議  *      protocol - 協議,一些特殊的套接字類型下可能會用到,但是做TCP或者UDP編程時不會用到此參數,所以我們通常傳遞0  */ int socket(int domain, int type, int protocol);  /*  * 功能:將socket綁定到指定的地址  * 返回:成功時,返回0;失敗時,返回-1,可以通過errno獲取錯誤類型  * 參數:  *      sockfd  - 套接字文件描述符,就是socket函數成功時返回的那個  *      addr    - 所要綁定到的地址,其中包含地址種類、協議族IP地址和端口號,IP地址和端口號在賦值時分別需要用htonl和htons函數進行大小端轉換  *      addrlen - 地址長度,因爲我們通常用的是sockaddr_in類型地址,所以這個參數就是sizeof(struct sockaddr_in)  */ int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  /*  * 功能:監聽套接字上的連接  * 返回:成功時,返回0;失敗時,返回-1,可以通過errno獲取錯誤類型  * 參數:  *      sockfd  - 套接字文件描述符,就是socket函數成功時返回的那個  *      backlog - 等待連接完成的隊列大小,當服務器繁忙時可能沒辦法一次響應所有連接請求,  *                 這時候連接請求會被放入隊列等待處理,隊列滿的時候,客戶端才真正無法連接  */ int listen(int sockfd, int backlog);  /*  * 功能:接受一個新的連接  * 返回:成功時,返回新連接的socket文件描述符;失敗時,返回-1,可以通過errno獲取錯誤類型  * 參數:  *      sockfd  - 監聽的套接字文件描述符,就是listen函數用的那個  *      addr    - 新連接的地址信息(指針返回)  *      addrlen - 新連接的地址長度(指針返回)  */ int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

上面的函數介紹並不是最詳細也不是最權威的,我建議大家不妨在命令行下面用man命令查閱各個函數的詳細文檔,用法是man [函數名]。

在bind之前我們創建socket地址的時候,用到了htonl和htons函數:

/*  * 功能:Host to Network (long),將主機上的長整型數據進行大小端轉換,以適應網絡規範  */ uint32_t htonl(uint32_t hostlong);  /*  * 功能:Host to Network (short),將主機上的短整型數據進行大小端轉換,以適應網絡規範  */ uint16_t htons(uint16_t hostshort);

分別與這兩個函數對應但沒有出現在代碼中的還有:

/*
 * 功能:Network to Host (long),將網絡規範格式的長整型數據進行大小端轉換,以適應主機
 */
uint32_t ntohl(uint32_t netlong);

/*
 * 功能:Network to Host (short),將網絡規範格式的短整型數據進行大小端轉換,以適應主機
 */
uint16_t ntohs(uint16_t netshort);

爲什麼數據需要進行大小端的轉換呢?大小端轉換又是什麼呢?這就要涉及到計算機組織原理的知識了。

簡單說來,我們的數據在計算機中是以二進制字節數據表示的,二進制字節數據保存在內存中時就涉及到一個實現問題,比如十進制數1,對應的字節是應該表示成1000 0000還是0000 0001呢?到底是高位在前還是低位在前對計算機來說是一個實現的問題,而我們平時閱讀和書寫的習慣是高位在前,所以成爲大端模式,即0000 0001格式,而反之則稱爲小端格式。在不同廠商的CPU上,數據的存儲格式是不一樣的,比如IBM和SUN用的是大端格式,Intel用的則是小端格式。而當不同數據格式的主機,在網絡間進行數據傳輸時,這個實現問題就演變成了兼容問題,而RFC規範中規定了網絡通訊時的字節格式是大端格式,所以系統就提供了相應的轉換函數提高程序兼容性。

對於大小端格式的詳細信息,大家如果有興趣瞭解可以在網上搜索,下面是一個關於大小端的有趣故事:

端模式(Endian)的這個詞出自Jonathan Swift書寫的《格列佛遊記》。

這本書根據將雞蛋敲開的方法不同將所有的人分爲兩類,從圓頭開始將雞蛋敲開的人被歸爲Big Endian,從尖頭開始將雞蛋敲開的人被歸爲Littile Endian。

小人國的內戰就源於吃雞蛋時是究竟從大頭(Big-Endian)敲開還是從小頭(Little-Endian)敲開。在計算機業Big Endian和Little Endian也幾乎引起一場戰爭。

在計算機業界,Endian表示數據在存儲器中的存放順序。

示例代碼中,在創建監聽的套接字文件描述符後,還執行了這樣一句代碼:

setsockopt(sd_listener_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

setsockopt函數是用來設置socket的參數的,這些參數決定socket的一些表現,例如後面的章節中我們使用這個函數設置socket爲無阻塞模式。而上面這行代碼則是用來運行socket重用端口的,所以它必須執行在bind之前。這麼設置爲了防止程序在意外退出後,系統沒有釋放端口而導致程序無法再使用原來端口

本章的示例代碼只需要走馬觀花看一遍,熟悉一下socket編程的大概流程,以後自己親手實驗的時候不記得怎麼做了再回來查閱就可以。

沒必要死記硬背這些API,只需要知到這些函數存在,它們大概幹什麼用的。蓋樓嘛,你背各種泥沙學名和化學成分有什麼用?只要懂得辨別,需要時能從手冊找到查閱到具體信息就可以了。

本章總結

本章演示了一個簡單的echo服務器,它只支持一次處理一個連接,並且在連接退出時,服務器端也跟着關閉。

通過這個簡單的例子,我們學習了基本的socket初始化過程和連接的響應方式。

當然這些知識對於我們的遠大目標“高性能socket服務器"來說是遠遠不夠的,但是這些是基礎的基礎,至少我們已經邁開了第一步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章