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接口函數

工作原理:“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


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