Stories

Detail Return Return

Socket編程基礎與QT的TCP通信 - Stories Detail

Socket編程基礎與QT的TCP通信

網絡編程的重要性

  • 單台計算機能做的工作非常有限,只有實現多台計算機的互聯互通,才能提供更加強大的功能。實現多台計算機之間的互通互連具有極大的實用價值。由於現在網絡的不斷髮展完善,通過網絡實現計算機的互通互連是一件簡單但及其重要的事。當前各種應用基本上都需要實現聯網功能,即學會網絡編程是一個程序員的基本要求。
  • 現在上網如此簡單,為什麼還要學習網絡編程。感覺上網簡單不代表不需要學習網絡編程知識,相反,這説明了網絡編程的適用範圍之廣、影響之深,更加説明了網絡編程的重要性。

Socket基礎知識

  • 提到網絡編程就不得不説著名的TCP/IP協議。即網絡節點由一個ip地址代表,加上端口號,就能標示某台機器中的某個進程。該協議的實用性得到了廣泛的驗證,並得到了極好的支持,即基本上大多數需要聯網的機器都支持該協議。為了能有效使用該協議實現各進程間的數據交互,我們通常使用一個名為Socket(嵌套字)的編程接口進行網絡編程。
  • Socket起源於Unix,故其被設計成一個“文件”,它支持文件的各種操作:創建,打開,讀寫,關閉等。在程序中可以將其視為一種特殊的文件,可以通過該文件實現不同機器之間的數據交互。
  • Socket不僅僅支持TCP協議,它是一個接口,通過該接口可以使用許多協議實現網絡通信。具體使用什麼協議,由Socket創建函數的參數確定。TCP協議只是被Socket所支持的協議的一種,不是唯一。不過本片博客是以TCP協議為例,初步學習認識Socket編程,進而學習如何進行網絡編程。

Socket通訊流程

  • Socket通信流程如下圖:
  • 創建Socket:創建Socket即得到一個Socket描述符,該描述符唯一標示一個Socket,以後對該Socket的操作大多需要使用其對應的Socket描述符號。

    • 使用函數為int socket(int domain, int type, int protocol);
    • domain為協議域,常見的有AF_INET(ipv4協議)、AF_INET6(ipv6協議)、AF_LOCAL (用於同一台機器上不同進程間進行通信)、AF_ROUTE(用於程序與系統內核進行數據交互)等。不同協議域的Socekt決定了其地址類型,在後面必須賦予正確類型的地址,該Socket才能用於通信。

      • 在TCP通信中,協議域選擇AF_INET(ipv4協議)或者AF_INET6(ipv6協議),地址為ip地址和端口號的組合。
    • type為Socket類型,常見的有SOCK_STREAM (流式嵌套字,主要用於面向連接的數據傳輸,如TCP協議)、 SOCK_DGRAM(數據包式嵌套字,主要用於非連接可靠數據傳輸,如UDP協議) 、 SOCK_RAW (原始網絡協議式嵌套字,用於調用更多網絡協議的數據報,如ICMP報文等,還支持修改報文頭等)、 SOCK_PACKET(網絡驅動式嵌套字,該嵌套字直接將數據從網卡傳給用户,即只支持網卡協議,不會安裝其它協議進行數據的預處理) 、 SOCK_SEQPACKET (可靠的數據包式嵌套字,即以數據包的形式交互數據,但是提供了確認機制以保障其可靠性)。

      • 在TCP通信中,類別一般選擇SOCK_STREAM。
    • protocol為此Socket所使用的協議常見的有IPPROTO_TCP(TCP協議)、IPPTOTO_UDP(UDP協議)、IPPROTO_SCTP(SCTP協議,可靠的面向控制的一個協議)、IPPROTO_TIPC(TIPC協議,一種適用於高可信網絡中或者集羣中的協議,即可靠性有網絡本身確定,這樣的網絡一般成本較高、範圍較小) 。該參數不是前面兩個參數的任意組合,只能是支持協議域的協議的Socket類型,即前兩個參數的某些正確的組合,如SOCK_STREAM不可以跟IPPROTO_UDP組合 。一般此值填0(表示選擇type值的默認協議)

      • 在TCP通信中,協議一般填IPPROTO_TCP。
    • TCP創建服務器監聽Socket的一個示例

      int listenfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
  • 綁定地址。當得到一個Socket後,需要根據該Socket的協議域綁定一個正確類型的地址給該Socket。只有綁定了地址,該Socket才能具有實際意義,才能用於通信。綁定地址這個操作是用户主動綁定還是由操作系統自己進行綁定會有不同的結果。當用户自己主動綁定,該Socket地址就可寫入代碼。在C/S模式中,有一個的服務器監聽Socket,這個Socket就需要用户主動綁定地址,這樣客户端通信Socket才能知道連接地址(即,服務器監聽Socket和服務器通信Socket以及客户端通信Socket本質上沒有差別,只是是否主動綁定地址而其使用方式不同)。而客户端的地址一般都是系統自己綁定,如果用户自己綁定地址,可能地址被其它進程使用,從而發生錯誤。

    • 使用函數為int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • sockfd表示想綁定地址的Socket描述字,即表示想給哪一個Socket綁定地址,一般為Socket()函數的返回值。
    • addr表示地址,不同的協議域的地址類型不一樣,要保證地址類型的正確性。

      • TCP協議中,地址類型為ip號和端口號的組合類型,較為友好的類型為struct sockaddr_in該類型中有數據成員.sin_family保存協議域,.sin_addr.s_addr保存ip地址,.sin_port保存端口號,一個該類型實例化例子

        struct sockaddr_in my_addr;//創建實例
        my_addr.sin_family=AF_INET;//指定協議域為ipv4協議域
        my_addr.sin_addr.s_addr=htonl(INADDR_ANY);//本地所有地址
        my_addr.sin_port=htons(8000);//端口號為8000
    • addrlen表示地址類型的長度,一般使用sizeof()獲取。
    • TCP協議中的地址綁定的一個完整示例

      struct sockaddr_in my_addr;//創建實例
      my_addr.sin_family=AF_INET;//指定協議域為ipv4協議域
      my_addr.sin_addr.s_addr=htonl(INADDR_ANY);//本地所有地址
      my_addr.sin_port=htons(8000);//端口號為8000
      bind(listenfd, (struct sockaddr *)&my_addr, sizeof(my_addr))
  • 監聽Socket使其變為被動類型的Socket。socket()創建的是默認為主動型的socket,而listen()函數可以將轉化為被動型socket,即需要等待其他socket連接它,而被動性Socket的地址一般是由用户主動綁定的,這樣其它Socket才能知道以什麼地址連接它。

    • 使用的函數為int listen(int sockfd, int backlog);
    • sockfd即想被轉化為被動型Socket的Socket描述字。
    • backlog表示可存儲的連接數量大小,即可以存儲多少個等待被用户取走使用的連接。不同協議或者嵌套字類型的連接含義不一樣。

      • TCP的被動型Socket將維護兩個隊列(SYN QUEUE和ACCEPT QUEUE),SYN QUEUE隊列保存未完成三次握手的連接,ACCEPT QUEUE隊列保存完成三次握手的連接。而隊列的大小由listen()函數的第二個參數確定。SYN QUEUE隊列的元素在完成三次握手後會被插入ACCEPT QUEUE中(前提是ACCEPT QUEUE還有空間),當ACCEPT QUEUE的元素數量達到最大值時,該Socket將暫停新的連接,直到ACCEPT QUEUE的元素被取走後,才能接受新的連接。
      • 監聽Socekt的示例listen(listenfd,10);//可存儲連接數為10
  • 其它Socket申請連接。當由Socket被置為監聽狀態後,其它Socket就可以申請連接以便後續形成通信對。這樣才能進行雙方的數據交互。

    • 使用函數int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
    • sockfd為申請連接到監聽Socket的Socket描述字。
    • servaddr為監聽Socket的地址。該地址一般由用户主動綁定,故可以被用户賦值以申請連接。同時地址類型要求如上,由該Socket的協議域確定。
    • 申請連接示例

      int clientSocket=Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
      connect(clientSocket,my_addr,sizeof(my_addr));
  • 提取連接形成通信對。當被動型Socket被其它Socket連接成功並被用户申請提供服務時,將會從該被動型Socket的已完成隊列中取出一個已完成連接並返回一個新的Socket以提供服務。申請連接的Socket與這個返回的Socket進行通信,而不是直接與監聽Socket進行通信。即在C/S模式中:經過主動綁定地址並置為監聽狀態的Socket(通常被稱為服務器端監聽Socket)不會參與直接通信,而是返回新的Socket(通常被稱為服務器通信Socket),其返回的Socket才被用來參與通信。

    • 使用的函數為int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    • sockfd為被置為監聽狀態的Socket的描述字。
    • addr表示申請連接的Socket的地址,這個地址內容一般由申請連接的Socket所在系統自己分配綁定,而監聽的Socket所返回的用於通信的Socket需要知道申請連接的Socket的地址。當然,當對申請連接的Socket的地址不感興趣(即在代碼中不會使用),可置為UNLL,這樣通信Socket所需信息由系統自己賦予,不用用户自己賦予。同時地址類型要求如上,由該Socket的協議域確定。
    • addrlen表示addr的長度,一般由sizeof獲取。當addr的值為NULL時,這個值也為NULL。
    • 形成通信對的示例

      int communicationSk=Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
      communicationSk=accept(int sockfd, NULL, NULL);//申請連接的地址相關操作由系統自己完成
  • 交互數據。通過上述操作,將會形成一對一對的通信對(Socket對),在通信對間就可以進行數據交互。以實現網絡通信的效果。但是交互的數據如何組織、是什麼含義、如何使用,由用户確定,可以使用出名的協議(如http),也可以自己定義。

    • 發送數據。即將特定區域的數據由Socket發送給對端的Socket。

      • 使用函數為ssize_t write (int socketfd, const void *buffer, size_t size)
      • socketfd。想發送數據的Socket的描述字。
      • buffer。存儲着想發送的數據的存儲區域。該區域存儲的數據的數據格式、內容由用户自己決定。
      • size。表示存儲區域的大小。可由sizeof確定。
      • 發送數據示例

        /*
        *通過Socket實例 clientSocket將my_data發送給clientSocket的對端*Socket,
        */
        std::string my_data('hello world');
        write(clientSocket,&my_data,sizeof(my_data));
    • 接受數據。當Socket被對端發送了數據後,可以將接受到的數據賦予到特定的區域,以可以被用户標記並自行使用。

      • 使用函數ssize_t read (int socketfd, void *buffer, size_t size)
      • socketfd,被髮送了數據的Socket的描述字。
      • buffer,保存所接受到的數據的存儲區域,通過buffer用户可以標示所接受到的數據,並在後續進行相關使用。
      • size,從Socket讀取的數據的大小。一般和buffer大小相同。
      • 接受數據示例

        /*
        *通過Socket實例communicationSk將my_data的數據內容存儲在*my_server_data
        */
        char my_server_data[12]=[]{'\0'};
        read(communicationSk,my_server_data,11);
  • 關閉Socket。由於Socket是一種特殊的文件,和其它文件一樣,當不需要時需要被關閉。關閉Socket有兩種常用的關閉方式。這兩種關閉方式效果不太一樣(是否影響其它進程的讀寫、是否會回收Socket資源),應用環境不太一樣(是否需要在半連接狀態單方面傳輸數據)。其間的差別需要深入學習,這裏只做了簡短的介紹。

    • 一種是close()函數,當多個進程共享同一個Socket時,該Socket會維護一個引用計數(即有多少個進程共享該Socket),在某個進程中調用close函數時,引用計數會減一,同時關閉該Socket在該進程的讀和寫,即該socket在該進程中用户不能主動使用讀寫函數(該進程的緩衝區中未發送或者接受的數據會先處理完,再根據引用計數是否為0選擇是否關閉連接,但是在close函數後用户不能在該進程中再調用讀寫函數進行額外的數據收發。但是其它共享該Socket的進程還是可以使用該Socket進行讀寫操作)。只有引用計數為0時,即所有共享該Socket的進程都不需要該Socket時,才會進行斷開連接操作(如TCP就會進行4次握手斷開連接操作)並進行Socket的各種資源的回收(如回收Socket的緩衝區等)。即close只是關閉一個進程中的Socket的讀寫,當所有進程都close後會回收資源。

      • 使用函數int close(int fd);
      • fd代表想要關閉的Socket的描述字。
      • 關閉socket示例

        close(communicationSk)
        /*由於只有一個進程使用communicationSk,調用這個函數後,系統會在處理  *好緩衝區中的數據後,進行4次握手斷開連接操作,如果有多個進程使用該*Socket時,此時不會斷開連接,直到所有進程都調用主動關閉才會斷開連接
        */
    • 一種是shutdown()。該函數可以指定關閉讀還是關閉寫,還是都關閉。但是是影響所有共享該Socket的進程的。即不管該Socket的引用計數的數量是否為0,所有進程都不能在該函數後調用讀或者寫或者都不能(具體情況由其指定的關閉方向確定)。但是Socket的資源不會回收,只有程序結束或者調用close後且引用計數為0時才會回收資源。

      • 使用函數int shutdown(int sockfd, int howto)
      • sockfd要關閉讀寫的Socket描述字
      • howto表示的關閉方向

        • howto=SHUT_RD(0) 時,關閉的是Socket的讀功能,即所有共享該Socket的進程都無法通過該socket讀取其它socket發送過來的數據。
        • howto=SHUT_WD(1) 時,關閉的是Socket的寫功能,即所有共享該Socket的進程都無法通過該socket發送數據給其它socket。
        • howto=SHUT_RDWD(2) 時,關閉的是Socket的讀寫功能,即所有共享該Socket的進程都無法通過該socket進行數據的收發。
  • C/S模式通信流程總結:服務器端:創建服務器監聽Socket(創建、綁定地址、監聽、有連接時返回服務器端通信Socket),客户端:申請連接(創建、連接)。服務器中有兩種Socket(這兩種Socket本質沒有差別,只是對Socket進行了不同操作從而形成不同種類的Socket)。同時Socket的關閉也需要根據不同的需求選擇不同的關閉方式。

QT的TCP通訊流程

  • QT有其獨特的信號槽機制,其TCP通信中也引入的信號槽機制。QT中,使用QTcpServer類創建服務器監聽Socket。使用QTcpSocket類創建用於通信的Socket。其通信流程大致如下:

    1. 創建監聽Socket(即在服務器端實例化一個QTcpServer對象)
    2. 調用QTcpServer對象的listen方法進行監聽,等待連接的到來。

      • 當有連接申請到來時,QTcpServer對像會發送一個newConnection信號,關聯該信號到一個槽函數(這個槽函數中一般會調用nextPendingConnection方法,該方法會返回一個QTcpSocket對象,即服務器端的通信Socket)。
    3. 創建客户端通信Socket(即客户端實例化一個QTcpSocket對象)

      • 客户端調用QTcpSocket對象的connectToHost方法申請連接。
    4. 交互數據。當通過上述步驟創建了通信對後,就可以進行數據交互了。

      • 當有數據到某個QTcpSocket對象時,該QTcpSocket對象會發送readyRead信號。關聯該信號到一個槽函數(這個槽函數一般會調用readAll或者其它讀方法,然後對讀取的數據進行後續處理)
      • 調用QTcpSocket的write函數發送數據。
    5. 關閉連接。當不需要TCP連接時,客户端調用QTcpSocket對象的disconnectFromHost方法申請斷開連接。

      • 此時服務器通信Socket會發送disconnected信號,將該信號連接相應的斷開連接槽函數。

QT的TCP通信實現多個客户端連接服務器的一種方法

  • 對單個客户端不用繼承QTcpServer(除非想在服務器使用單例模式,以確保只有一個服務器實例,則需要繼承QTcpServer)和QTcpSocket,但對多個客户端時有一個處理方法就是:繼承上述兩個類,因為需要知道是哪一個socket發來的信息,故需要將socket的信息處理函數封裝一下,同時由於派生了QTcpSocket,如果需要使用派生類,就需要使用QTcpServer的虛函數incomingConnection(這是一個當有客户端申請連接時,就會執行的函數)來產生自定義的socket,故QTcpServer也需要進行派生以重寫虛函數incomingConnection。

Add a new Comments

Some HTML is okay.