<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    海闊天空

    I'm on my way!
    隨筆 - 17, 文章 - 69, 評論 - 21, 引用 - 0
    數據加載中……

    經典的Linux Socket 編程 示例代碼 (上)

    http://fanqiang.chinaunix.net/a4/b7/20010810/1200001101.html


    Linux是一個可靠性非常高的操作系統,但是所有用過Linux的朋友都會感 覺到, Linux和Windows這樣的"傻瓜"操作系統(這里絲毫沒有貶低Windows的意思,相反這應該是Windows的優點)相比,后者無疑在易操作 性上更勝一籌。但是為什么又有那么多的愛好者鐘情于Linux呢,當然自由是最吸引人的一點,另外Linux強大的功能也是一個非常重要的原因,尤其是 Linux強大的網絡功能更是引人注目。放眼今天的WAP業務、銀行網絡業務和曾經紅透半邊天的電子商務,都越來越倚重基于Linux的解決方案。因此 Linux網絡編程是非常重要的,而且當我們一接觸到Linux網絡編程,我們就會發現這是一件非常有意思的事情,因為以前一些關于網絡通信概念似是而非 的地方,在這一段段代碼面前馬上就豁然開朗了。在剛開始學習編程的時候總是讓人感覺有點理不清頭緒,不過只要多讀幾段代碼,很快我們就能體會到其中的樂趣 了。下面我就從一段Proxy源代碼開始,談談如何進行Linux網絡編程。

       首先聲明,這段源代碼不是我編寫的,讓我們感謝這位名叫Carl Harris的大蝦,是他編寫了這段代碼并將其散播到網上供大家學習討論。這段代碼雖然只是描述了最簡單的proxy操作,但它的確是經典,它不僅清晰地 描述了客戶機/服務器系統的概念,而且幾乎包括了Linux網絡編程的方方面面,非常適合Linux網絡編程的初學者學習。
    這段Proxy程序的用法是這樣的,我們可以使用這個proxy登錄其它主機的服務端口。假如編譯后生成了名為Proxy的可執行文件,那么命令及其參數的描述為:
    ./Proxy <proxy_port> <remote_host> <service_port>
    其中參數proxy_port是指由我們指定的代理服務器端口。參數remote_host是指我們希望連接的遠程主機的主機名,IP地址也同樣有 效。這個主機名在網絡上應該是唯一的,如果您不確定的話,可以在遠程主機上使用uname -n命令查看一下。參數service_port是遠程主機可提供的服務名,也可直接鍵入服務對應的端口號。這個命令的相應操作是將代理服務器的 proxy_port端口綁定到remote_host的service_port端口。然后我們就可以通過代理服務器的proxy_port端口訪問 remote_host了。例如一臺計算機,網絡主機名是legends,IP地址為10.10.8.221,如果在我的計算機上執行:
    [root@lee /root]#./proxy 8000 legends telnet
    那么我們就可以通過下面這條命令訪問legends的telnet端口。
    -----------------------------------------------------------------
    [root@lee /root]#telnet legends 8000
    Trying 10.10.8.221...
    Connected to legends(10.10.8.221).
    Escape character is '^]'

    Red Hat Linux release 6.2(Zoot)
    Kernel 2.2.14-5.0 on an i686
    Login:
    -----------------------------------------------------------------
    上面的綁定操作也可以使用下面的命令:
    [root@lee /root]#./proxy 8000 10.10.8.221 23
    23是telnet服務的標準端口號,其它服務的對應端口號我們可以在/etc/services中查看。

    下面我就從這段代碼出發談談我對Linux網絡編程的一些粗淺的認識,不對的地方還請各位大蝦多多批評指正。

    ◆main()函數
    -----------------------------------------------------------------
    #include <stdio.h>
    #include <ctype.h>
    #include <errno.h>
    #include <signal.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <sys/file.h>
    #include <sys/ioctl.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <netdb.h>
    #define TCP_PROTO   "tcp"
    int proxy_port;    /* port to listen for proxy connections on */
    struct sockaddr_in hostaddr;   /* host addr assembled from gethostbyname() */
    extern int errno;   /* defined by libc.a */
    extern char *sys_myerrlist[];
    void parse_args (int argc, char **argv);
    void daemonize (int servfd);
    void do_proxy (int usersockfd);
    void reap_status (void);
    void errorout (char *msg);
    /*This is my modification.
    I'll tell you why we must do this later*/
    typedef void Signal(int);
    /****************************************************************
    function:    main
    description:   Main level driver. After daemonizing the process, a socket is opened to listen for         connections on the proxy port, connections are accepted and children are spawned to         handle each new connection.
    arguments:    argc,argv you know what those are.
    return value:  none.
    calls:      parse_args, do_proxy.
    globals:     reads proxy_port.
    ****************************************************************/
    main (argc,argv)
    int argc;
    char **argv;
    {
    int clilen;
    int childpid;
    int sockfd, newsockfd;
    struct sockaddr_in servaddr, cliaddr;
    parse_args(argc,argv);
    /* prepare an address struct to listen for connections */
    bzero((char *) &servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = proxy_port;
    /* get a socket... */
    if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) {
    fputs("failed to create server socket\r\n",stderr);
    exit(1);
    }
    /* ...and bind our address and port to it */
    if   (bind(sockfd,(struct sockaddr_in *) &servaddr,sizeof(servaddr)) < 0) {
    fputs("faild to bind server socket to specified port\r\n",stderr);
    exit(1);
    }
    /* get ready to accept with at most 5 clients waiting to connect */
    listen(sockfd,5);
    /* turn ourselves into a daemon */
    daemonize(sockfd);
    /* fall into a loop to accept new connections and spawn children */
    while (1) {
    /* accept the next connection */
    clilen = sizeof(cliaddr);
    newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);
    if (newsockfd < 0 && errno == EINTR)
    continue;
    /* a signal might interrupt our accept() call */
    else if (newsockfd < 0)
    /* something quite amiss -- kill the server */
    errorout("failed to accept connection");
    /* fork a child to handle this connection */
    if ((childpid = fork()) == 0) {
    close(sockfd);
    do_proxy(newsockfd);
    exit(0);
    }
    /* if fork() failed, the connection is silently dropped -- oops! */
    lose(newsockfd);
    }
    }
    -----------------------------------------------------------------
    上面就是Proxy源代碼的主程序部分,也許您在網上也曾經看到過這段代碼,不過細心的您會發現在上面這段代碼中我修改了兩個地方,都是在預編譯部分。一個地方是在定義外部字符型指針數組時,我將原代碼中的
    extern char *sys_errlist[];
    修改為
    extern char *sys_myerrlist[];原因是在我的Linux環境下頭文件"stdio.h"已經對sys_errlist[]進行了如下定義:
    extern __const char *__const sys_errlist[];
    也許Carl Harris在94年編寫這段代碼時系統還沒有定義sys_errlist[],不過現在我們不修改一下的話,編譯時系統就會告訴我們sys_errlist發生了定義沖突。
    另外我添加了一個函數類型定義:
    typedef void Sigfunc(int);
    具體原因我將在后面向大家解釋。

    套接字和套接字地址結構定義

    這段主程序是一段典型的服務器程序。網絡通訊最重要的就是套接字的使用,在程序的一開始就對套接字描述符sockfd和newsockfd進行了定義。 接下來定義客戶機/服務器的套接字地址結構cliaddr和servaddr,存儲客戶機/服務器的有關通信信息。然后調用parse_args (argc,argv)函數處理命令參數。關于這個parse_args()函數我們待會兒再做介紹。

    創建通信套接字

      下面就是建立一個服務器的詳細過程。服務器程序的第一個操作是創建一個套接字。這是通過調用函數socket()來實現的。socket()函數的具體描述為:
    -----------------------------------------------------------------
    #include <sys/types.h>
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
    -----------------------------------------------------------------
    參數domain指定套接字使用的協議族,AF_INET表示使用TCP/IP協議族,AF_UNIX表示使用Unix協議族,AF_ISO表示套接 字使用ISO協議族。type指定套接字類型,一般的面向連接通信類型(如TCP)設置為SOCK_STREAM,當套接字為數據報類型時,type應設 置為SOCK_DGRAM,如果是可以直接訪問IP協議的原始套接字則type應設置為SOCK_RAW。參數protocol一般設置為"0",表示使 用默認協議。當socket()函數成功執行時,返回一個標志這個套接字的描述符,如果出錯則返回"-1",并設置errno為相應的錯誤類型。

    設置服務器套接字地址結構

    在通常情況下,首先要將描述服務器信息的套接字地址結構清零,然后在地址結構中填入相應的內容,準備接受客戶機送來的連接建立請求。這個清零操作可以用 多種字節處理函數來實現,例如bzero()、bcopy()、memset()、memcpy()等,以字母"b"開始的兩個函數是和BSD系統兼容 的,而后面兩個是ANSI C提供的函數。這段代碼中使用的bzero()其描述為:
    void bzero(void *s, int n);
    函數的具體操作是將參數s指定的內存的前n個字節清零。memset()同樣也很常用,其描述為:
    void *memset(void *s, int c, size_t n);
    具體操作是將參數s指定的內存區域的前n個字節設置為參數c的內容。
    下一步就是在已經清零的服務器套接字地址結構中填入相應的內容。Linux系統的套接字是一個通用的網絡編程接口,它應該支持多種網絡通信協議,每一 種協議都使用專門為自己定義的套接字地址結構(例如TCP/IP網絡的套接字地址結構就是struct sockaddr_in)。不過為了保持套接字函數調用參數的一致性,Linux系統還定義了一種通用的套接字地址結構:
    -----------------------------------------------------------------
    <linux/socket.h>
    struct sockaddr
    {
    unsigned short sa_family; /* address type */
    char sa_data[14]; /* protocol address */
    }
    -----------------------------------------------------------------
    其中sa_family意指套接字使用的協議族地址類型,對于我們的TCP/IP網絡,其值應該是AF_INET,sa_data中存儲具體的協議地 址,不同的協議族有不同的地址格式。這個通用的套接字地址結構一般不用做定義具體的實例,但是常用做套接字地址結構的強制類型轉換,如我們經常可以看到這 樣的用法:
    bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))
    用于TCP/IP協議族的套接字地址結構是sockaddr_in,其定義為:
    -----------------------------------------------------------------
    <linux/in.h>
    struct in_addr
    {
    __u32 s_addr;
    };
    struct sochaddr_in
    {
    short int sin_family;
    unsigned short int sin_port;
    struct in_addr sin_addr;
    /*This part has not been taken into use yet*/
    nsigned char_ _ pad[_ _ SOCK_SIZE__- sizeof(short int) -sizeof(unsigned short int) -       sizeof(struct in_addr)];
    };
    #define sin_zero_ - pad
    -----------------------------------------------------------------
    其中sin_zero成員并未使用,它是為了和通用套接字地址struct sockaddr兼容而特意引入的。在編程時,一般都通過bzero()或是memset()將其置零。其他成員的設置一般是這樣的:
    servaddr.sin_family = AF_INET;
    表示套接字使用TCP/IP協議族。
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    設置服務器套接字的IP地址為特殊值INADDR_ANY,這表示服務器愿意接收來自任何網絡設備接口的客戶機連接。htonl()函數的意思是將主機順序的字節轉換成網絡順序的字節。
    servaddr.sin_port = htons(PORT);
    設置通信端口號,PORT應該是我們已經定義好的。在本例中servaddr.sin_port = proxy_port;這是表示端口號是函數的返回值proxy_port。
    另外需要說明的一點是,在本例中,我們并沒有看到在預編譯部分中包含有<linux/socket.h>和< linux/in.h>這兩個頭文件,那是因為這兩個頭文件已經分別被包含在<sys/types.h>和< sys/types.h>中了,而且后面這兩個頭文件是與平臺無關的,所以在網絡通信中一般都使用這兩個頭文件。

    服務器公開地址

      如果服務器要接受客戶機的連接請求,那么它必須先要在整個網絡上公開自己的地址。在設置了服務器的套接字地址結構之后,可以通過調用函數bind()綁定服務器的地址和套接字來完成公開地址的操作。函數bind()的詳細描述為:
    -----------------------------------------------------------------
    #include <sys/types.h>
    #include <sys/socket.h>
    int bind(int sockfd, struct sockaddr *addr, int addrlen);
    -----------------------------------------------------------------
    參數sockfd是我們通過調用socket()創建的套接字描述符。參數addr是本機地址,參數addrlen是套接字地址結構的長度。函數執行成功時返回"0",否則返回"-1",并設置errno變量為EADDRINUAER。
    如果是服務器調用bind()函數,如果設置了套接字的IP地址為某個本地IP地址,那么這表示服務器只接受來自于這個IP地址的特定主機發出的連接 請求。不過一般情況下都是將IP地址設置為INADDR_ANY,以便接受所有網絡設備接口送來的連接請求。
    客戶機一般是不會調用bind()函數的,因為客戶機在連接時不用指定自己的套接字地址端口號,系統會自動為客戶機選擇一個未用端口號,并且用本地 IP地址自動填充客戶機套接字地址結構中的相應項。但是在某些特定的情況下客戶機需要使用特定的端口號,例如Linux中的rlogin命令就要求使用保 留端口號,而系統是不能為客戶機自動分配保留端口號的,這就需要調用bind()來綁定一個保留端口號了。不過在一些特殊的環境下,這樣綁定特定端口號也 會帶來一些負面影響,如在HTTP服務器進入TIME_WAIT狀態后,客戶機如果要求再次與服務器建立連接,則服務器會拒絕這一連接請求。如果客戶機最 后進入TIME_WAIT狀態,則馬上再次執行bind()函數時會返回出錯信息"-1",原因是系統會認為同時有兩次連接綁定同一個端口。

    轉換Listening套接字

    接下來,服務器需要將我們剛才與IP地址和端口號完成綁定的套接字轉換成傾聽listening套接字。只有服務器程序才需要執行這一步操作。我們通過調用函數listen()實現這一操作。listen()的詳細描述為:
    -----------------------------------------------------------------
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
    -----------------------------------------------------------------
    參數sockfd指定我們要求轉換的套接字描述符,參數backlog設置請求隊列的最大長度。函數listen()主要完成以下操作。
    首先是將套接字轉換成傾聽套接字。因為函數socket()創建的套接字都是主動套接字,所以客戶機可以通過調用函數connect()來使用這樣的 套接字主動和服務器建立連接。而服務器的情況恰恰相反,服務器需要通過套接字接收客戶機的連接請求,這就需要一個"被動"套接字。listen()就可將 一個尚未連接的主動套接字轉換成為這樣的"被動"套接字,也就是傾聽套接字。在執行了listen()函數之后,服務器的TCP就由CLOSED變成 LISTEN狀態了。
    另外listen()可以設置連接請求隊列的最大長度。雖然參數backlog的用法非常簡單,只是一個簡單的整數。但搞清楚請求隊列的含義對理解TCP 協議的通信過程建立非常重要。TCP協議為每個傾聽套接字實際上維護兩個隊列,一個是未完成連接隊列,這個隊列中的成員都是未完成3次握手的連接;另一個 是完成連接隊列,這個隊列中的成員都是雖然已經完成了3次握手,但是還未被服務器調用accept()接收的連接。參數backlog實際上指定的是這個 傾聽套接字完成連接隊列的最大長度。在本例中我們是這樣用的:listen(sockfd,5);表示完成連接隊列的最大長度為5。

    接收連接

    接下來我們在主程序中看到通過名為daemonize()的自定義函數創建一個守護進程,關于這個daemonize()以及守護進程的相關概念,我 們等一會兒再做詳細介紹。然后服務器程序進入一個無條件循環,用于監聽接收客戶機的連接請求。在此過程中如果有客戶機調用connect()請求連接,那 么函數accept()可以從傾聽套接字的完成連接隊列中接受一個連接請求。如果完成連接隊列為空,這個進程就睡眠。accept()的詳細描述為:
    -----------------------------------------------------------------
    #include <sys/socket.h>
    int accept(int sockfd, struct sockaddr *addr, int *addrlen);
    -----------------------------------------------------------------
    參數sockfd是我們轉換成功的傾聽套接字描述符;參數addr是一個指向套接字地址結構的指針,參數addrlen為一個整型指針。當函數成功執 行時,返回3個結果,函數返回一個新的套接字描述符,服務器可以通過這個新的套接字描述符和客戶機進行通信。參數addr所指向的套接字地址結構中將存放 客戶機的相關信息,addrlen指針將描述前述套接字地址結構的長度。在通常情況下服務器對這些信息不是很感興趣,因此我們經常可以看到一些源代碼中將 accept()函數的后兩個參數都設置為NULL。不過在這段proxy源代碼中需要用到有關的客戶機信息,因此我們看到通過執行
    newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);
    將客戶機的詳細信息存放在地址結構cliaddr中。而proxy就通過套接字newsockfd與客戶機進行通信。值得注意的是這個返回的套接字描 述符與我們轉換的傾聽套接字是不同的。在一段服務器程序中,可以始終只用一個傾聽套接字來接收多個客戶機的連接請求;而如果我們要和客戶機建立一個實際的 連接的話,對每一個請求我們都需要調用accept()返回一個新的套接字。當服務器處理完畢客戶機的請求后,一定要將相應的套接字關閉;如果整個服務器 程序將要結束,那么一定要將傾聽套接字關閉。
    如果accept()函數執行失敗,則返回"-1",如果accept()函數阻塞等待客戶機調用connect()建立連接,進程在此時恰好捕捉到 信號,那么函數在返回"-1"的同時將變量errno的值設置為EINTR。這和accept()函數執行失敗是有區別的。因此我們在代碼中可以看到這樣 的語句:
    -----------------------------------------------------------------
    if (newsockfd < 0 && errno == EINTR)
    continue;
    /* a signal might interrupt our accept() call */
    else if (newsockfd < 0)
    /* something quite amiss -- kill the server */
    errorout("failed to accept connection");
    -----------------------------------------------------------------
    可以看出程序在處理這兩種情況時操作是完全不同的,同樣是accept()返回"-1",如果有errno == EINTR,那么系統將再次調用accept()接受連接請求,否則服務器進程將直接結束。

    posted on 2009-11-17 21:13 石頭@ 閱讀(2384) 評論(0)  編輯  收藏


    只有注冊用戶登錄后才能發表評論。


    網站導航:
     
    主站蜘蛛池模板: 中国黄色免费网站| 国产一级大片免费看| 中国一级特黄高清免费的大片中国一级黄色片| 亚洲精品日韩专区silk| 亚洲中文字幕日产乱码高清app| 德国女人一级毛片免费| 57pao一国产成视频永久免费| 国产精品成人69XXX免费视频| 九九精品国产亚洲AV日韩| 亚洲卡一卡2卡三卡4麻豆| 亚洲国产一区二区a毛片| 亚洲综合精品香蕉久久网| 四虎国产精品免费久久影院| 国产在线国偷精品产拍免费| 久久久久久夜精品精品免费啦| 美女巨胸喷奶水视频www免费| 黄色网址在线免费观看| 亚洲精品无码日韩国产不卡av| 亚洲白色白色永久观看| 久久久久亚洲AV片无码下载蜜桃| 亚洲欭美日韩颜射在线二| 国产亚洲精品无码拍拍拍色欲| 亚洲AV无码专区日韩| 亚洲?V无码乱码国产精品| 国产免费变态视频网址网站| 日韩免费在线观看| 卡1卡2卡3卡4卡5免费视频| 最近中文字幕免费mv视频7| 在线观看免费人成视频| 国产成人免费网站| 免费无码AV电影在线观看| 桃子视频在线观看高清免费完整 | 91精品视频免费| **毛片免费观看久久精品| 99精品视频在线观看免费播放| 久久精品视频免费| 99re6热视频精品免费观看| 国产曰批免费视频播放免费s | 亚洲免费中文字幕| 亚洲中文字幕无码久久2020| 亚洲色欲色欲www在线播放|