socket編程基礎

TCP/IP、UDP設計模式

  • TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業標準的協議集,它是爲廣域網(WANs)設計的。    數組

  • TCP/IP協議存在於OS中,網絡服務經過OS提供,在OS中增長支持TCP/IP的系統調用——Berkeley套接字,如Socket,Connect,Send,Recv等瀏覽器

  • UDP(User Data Protocol,用戶數據報協議)是與TCP相對應的協議。它是屬於TCP/IP協議族中的一種。如圖:服務器

wKioL1el3XSBe8W2AAB9ilaACCg612.jpg

TCP/IP協議族包括運輸層、網絡層、鏈路層,而socket所在位置如圖,Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層。網絡

wKioL1el3ZbSItboAACSZKmYUWo455.jpg

socket套接字

socket起源於Unix,而Unix/Linux基本哲學之一就是「一切皆文件」,均可以用「打開open –> 讀寫write/read –> 關閉close」模式來操做。Socket就是該模式的一個實現,         socket便是一種特殊的文件,一些socket函數就是對其進行的操做(讀/寫IO、打開、關閉).
     說白了Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。

數據結構

套接字描述符

當應用程序要建立一個套接字時,操做系統就返回一個小整數做爲描述符,應用程序則使用這個描述符來引用該套接字須要I/O請求的應用程序請求操做系統打開一個文件。操做系統就建立一個文件描述符提供給應用程序訪問文件。從應用程序的角度看,文件描述符是一個整數,應用程序能夠用它來讀寫文件。下圖顯示,操做系統如何把文件描述符實現爲一個指針數組,這些指針指向內部數據結構。wKiom1el31zQzI07AAB4n9aNUCM303.jpg

對於每一個程序系統都有一張單獨的表。精確地講,系統爲每一個運行的進程維護一張單獨的文件描述符表。當進程打開一個文件時,系統把一個指向此文件內部數據結構的指針寫入文件描述符表,並把該表的索引值返回給調用者 。應用程序只需記住這個描述符,並在之後操做該文件時使用它。操做系統把該描述符做爲索引訪問進程描述符表,經過指針找到保存該文件全部的信息的數據結構。socket

SOCKET接口函數ide

工做原理:「open—write/read—close」模式。函數

wKiom1el4JCzAsdvAABn91rWDBU000.jpg

服務器端先初始化Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束。測試

socket()函數

int  socket(int protofamily, int type, int protocol);

socket函數對應於普通文件的打開操做。普通文件的打開操做返回一個文件描述字,而socket()用於建立一個socket描述符(socket descriptor),它惟一標識一個socket。這個socket描述字跟文件描述字同樣,後續的操做都有用到它,把它做爲參數,經過它來進行一些讀寫操做。正如能夠給fopen的傳入不一樣參數值,以打開不一樣的文件。建立socket的時候,也能夠指定不一樣的參數建立不一樣的socket描述符,socket函數的三個參數分別爲:

  • protofamily:即協議域,又稱爲協議族(family)。經常使用的協議族有,AF_INET(IPV4)AF_INET6(IPV6)AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。

  • type:指定socket類型。經常使用的socket類型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等。

  • protocol:故名思意,就是指定協議。經常使用的協議有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議(通常設置爲0,讓系統自動選擇相應協議。)。

bind()函數

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。函數的三個參數分別爲:


  • sockfd:即socket描述字,它是經過socket()函數建立了,惟一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。

  • addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不一樣而不一樣,如ipv4對應的是: 

    struct sockaddr_in {
       sa_family_t    sin_family; /* address family: AF_INET */
       in_port_t      sin_port;   /* port in network byte order */
       struct in_addr sin_addr;   /* internet address */
    };

    /* Internet address. */
    struct in_addr {
       uint32_t       s_addr;     /* address in network byte order */
    };
  • addrlen:對應的是地址的長度。

一般服務器在啓動的時候都會綁定一個衆所周知的地址(如ip地址+端口號),用於提供服務,客戶就能夠經過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是爲何一般服務器端在listen以前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。

注意:

網絡字節序以大端模式傳輸。因此:在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序


listen()、connect()函數

做爲一個服務器,在調用socket()、bind()以後就會調用listen()來監聽這個socket,若是客戶端這時調用connect()發出鏈接請求,服務器端就會接收到這個請求。

int listen(int sockfd, int backlog);

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函數的第一個參數即爲要監聽的socket描述字,第二個參數爲相應socket能夠排隊的最大鏈接個數。socket()函數建立的socket默認是一個主動類型的,listen函數將socket變爲被動類型的,等待客戶的鏈接請求。

connect函數的第一個參數即爲客戶端的socket描述字,第二參數爲服務器的socket地址,第三個參數爲socket地址的長度。客戶端經過調用connect函數來創建與TCP服務器的鏈接。

accept()函數

TCP服務器端依次調用socket()、bind()、listen()以後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()以後就向TCP服務器發送了一個鏈接請求。TCP服務器監聽到這個請求以後,就會調用accept()函數取接收請求,這樣鏈接就創建好了。以後就能夠開始網絡I/O操做了,即類同於普通文件的讀寫I/O操做。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

  • 參數sockfd

    參數sockfd就是上面解釋中的監聽套接字,這個套接字用來監聽一個端口,當有一個客戶與服務器鏈接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關聯。固然客戶不知道套接字這些細節,它只知道一個地址和一個端口號。

  • 參數addr

    這是一個輸出型參數,它用來接受一個返回值,這返回值指定客戶端的地址,固然這個地址是經過某個地址結構來描述的,用戶應該知道這一個什麼樣的地址結構。若是對客戶的地址不感興趣,那麼能夠把這個值設置爲NULL。

  • 參數len

    如同你們所認爲的,它也是輸出型參數,用來接受上述addr的結構的大小的,它指明addr結構所佔有的字節個數。一樣的,它也能夠被設置爲NULL。


若是accept成功返回,則服務器與客戶已經正確創建鏈接了,此時服務器經過accept返回的套接字來完成與客戶的通訊。

注意

      accept默認會阻塞進程,直到有一個客戶鏈接創建後返回,它返回的是一個新可用的套接字,這個套接字是鏈接套接字。

read()、write()等函數

  • read()/write()

  • recv()/send()

  • readv()/writev()

  • recvmsg()/sendmsg()

  • recvfrom()/sendto()

我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上能夠把上面的其它函數都替換成這兩個函數。它們的聲明以下:

       #include <unistd.h>

      ssize_t read(int fd, void *buf, size_t count);
      ssize_t write(int fd, const void *buf, size_t count);

      #include <sys/types.h>
      #include <sys/socket.h>

      ssize_t send(int sockfd, const void *buf, size_t len, int flags);
      ssize_t recv(int sockfd, void *buf, size_t len, int flags);

      ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                     const struct sockaddr *dest_addr, socklen_t addrlen);
      ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                       struct sockaddr *src_addr, socklen_t *addrlen);

      ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
      ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

這幾個函數比較簡單,就不做詳細介紹了。

close()函數

在服務器與客戶端創建鏈接以後,會進行一些讀寫操做,完成了讀寫操做就要關閉相應的socket描述字,比如操做完打開的文件要調用fclose關閉打開的文件。

#include <unistd.h>
int close(int fd);

close一個TCP socket的缺省行爲時把該socket標記爲以關閉,而後當即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再做爲read或write的第一個參數。

注意:close操做只是使相應socket描述字的引用計數-1,只有當引用計數爲0的時候,纔會觸發TCP客戶端向服務器發送終止鏈接請求。

示例:

TCP通訊

服務器

//sever.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>


void* handler_data(void* arg)
{
    int sock = *((int*)arg);
    printf("connect a new client    %d\n", sock);

    char buf[1024];
    memset(buf, '\0', sizeof(buf));
    while(1)
    {
        ssize_t _s = read(sock, buf, sizeof(buf)-1);
        if(_s > 0)
        {
            buf[_s] = '\0';
            printf("client[%d] # %s\n", sock, buf);

            write(sock, buf, strlen(buf));
        }
        else if(_s == 0)
        {
            printf("client[%d] is closed...\n", sock);
            break;
        }
        else
        {
            break;
        }
    }

    close(sock);
    pthread_exit(NULL);
}

int main()
{
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock < 0)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(8080);
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    if(bind(listen_sock, (const struct sockaddr*)&local, sizeof(local)) < 0)
    {
        perror("bind");
        return 2;
    }

    if(listen(listen_sock, 5) < 0)
    {
        perror("listen");
        return 3;
    }

    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    while(1)
    {
        int new_fd = accept(listen_sock, (struct sockaddr*)&peer, &len);
        if(new_fd > 0)
        {
            pthread_t id;
            pthread_create(&id, NULL, handler_data, (void* )&new_fd);
            pthread_detach(id);
        }

    }

    return 0;
}

客戶端

//client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        printf("error argv\n");
        return 1;
    }

    int conn_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(conn_sock < 0)
    {
        perror("socket");
        return 2;
    }

    struct sockaddr_in remote;
    remote.sin_family = AF_INET;
    remote.sin_port = htons(atoi(argv[2]));
    remote.sin_addr.s_addr = inet_addr(argv[1]); 
    if(connect(conn_sock, (const struct sockaddr*)&remote, sizeof(remote)) < 0)
    {
        perror("connect");
        return 3;
    }

    char buf[1024];
    memset(buf, '\0', sizeof(buf));
    while(1)
    {
        printf("please enter# ");
        fflush(stdout);
        ssize_t _s = read(0, buf, sizeof(buf)-1);
        if(_s > 0)
        {
            buf[_s-1] = '\0';
            write(conn_sock, buf, strlen(buf));

           read(conn_sock, buf, sizeof(buf));
           printf("sever echo# %s\n", buf);
        }
    }

    return 0;
}

程序演示:

運行服務器後,服務器等待TCP鏈接,這裏能夠用三種方式測試:Telnet、瀏覽器、客戶端。

Telnet測試:

wKioL1el6k2CX-1tAABIN7Mkc9c614.png

瀏覽器測試:

wKiom1el6uPz_uarAACJ4459mPc110.png

客戶端測試:

wKioL1el62TRrBAkAABGLd87tAY022.png

注意:在啓動服務器的時候可能會出現以下的狀況:

wKioL1el7Bbx6J65AAATcpzWDjI172.png

如今用Ctrl-C把client終止掉,等待大約30秒後,服務器又能夠啓動了。

緣由分析:

雖然server的應用程序終止了,但TCP協議層的鏈接並無徹底斷開,所以不能再次監 聽一樣的server端口。

client終止時自動關閉socket描述符,server的TCP鏈接收到client發送的FIN段後處於TIME_WAIT狀態。TCP協議規定,主動關閉鏈接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間後才能回到CLOSED狀態,由於咱們先Ctrl-C終止了server,因此server是主動關閉鏈接的一方,在TIME_WAIT期間仍然不能再次監聽一樣的server端口。MSL在RFC1122中規定爲兩分鐘,可是各操做系統的實現不一樣,在Linux上通常通過半分鐘後就能夠再次啓動server了。


解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR爲1,表示容許建立端口號相同但IP地址不一樣的多個socket描述符。在server代碼的socket()和bind()調用之間插入以下代碼:


wKioL1el7cXyFS-6AAAnLKUhiwM451.png

setsocketopt這個函數這裏不做詳細介紹,有興趣的讀者能夠自行查詢一下。


j_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gifj_0015.gif

相關文章
相關標籤/搜索