1. 程式人生 > >socket程式設計(一)使用SOCK_STREAM建立可靠通訊

socket程式設計(一)使用SOCK_STREAM建立可靠通訊

socket是我們用來進行網路程式設計的基本API,一般系統都提供了socket,unix以及類unix(Linux、mac)它們都提供了socket,不過不同平臺還是有那點區別的,其中Windows區別最大了。本文的程式碼是在mac上測試通過的。

socket是一個應用層程式設計API,提供了tcp/ip四層模型的第三層傳輸層的TCP、UDP協議的資料傳輸方式。第二層網際層有IP協議,它本來是不可靠的協議,而TCP在它的基礎上提供了可靠傳輸。UDP仍然提供不可靠傳輸。

兩個程序若想通訊,可以通過socket來進行,不管這兩個程序在什麼位置,只要它們的主機都實現了TCP/IP協議棧。

我們用網路地址(ip)+port來標識一個通訊實體,A要找到B,只需要知道B的ip:port即可。若B處理的是TCP報文,那麼A應該指定傳輸協議是tcp,要是指定個udp或則其它協議,那麼B得到了傳輸層發給應用層的報文將是一個非TCP報文,B在傳輸層會丟棄這個報文。

傳輸層TCP報文只用到了port,網際層IP報文用到了ip地址,同時把TCP報文作為它的部分報文內容。

下面是TCP報文,有源port與目的port。報文字身說明了它是TCP報文,在socket中我們如果用TCP協議,那麼我們要指定socket用TCP協議。

下面是ip報文,有源ip與目的ip。

根據上面兩個報文我們可以看出socket不是一個工作在哪個層上的API,而是一個跨層的網路程式設計API,為應用層提供服務,我們可以開發一些網路層協議。

伺服器端:

socket程式設計,流程很固定。TCP傳輸模式是伺服器端先建立一個socket,然後把這個socket繫結到一個地址上。這個時候socket可以工作了,讓它向tcp/ip協議棧請求一個監聽服務並建立一個服務佇列來接受那些請求。這個服務佇列是什麼樣子的,我們看下伺服器怎麼跟客戶端通過三次握手接受請求並且建立連線的。

第一次

第一次握手:建立連線時,客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SENT狀態,等待伺服器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。

第二次

第二次握手:伺服器收到syn包,必須確認客戶的SYNack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;

第三次

第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHEDTCP連線成功)狀態,完成三次l握手。

上面三次握手後,客戶端與伺服器可以開始通訊了,而且通訊是可靠的。那麼服務佇列到底是什麼呢?它實際是一個未連線佇列。這個佇列如果每次跟客戶端只有第一次握手,

後面客戶端不給它第二次握手,而且大量的客戶端都這樣做,你猜會這麼樣?哈哈,未連線佇列裡將都是Syn_RECV狀態,它達不到ESTABLISHED狀態,不會被刪除,

這個佇列就會滿了,然後其它客戶端不能跟伺服器通訊了,這個其實是一種dos攻擊。

在三次握手協議中,伺服器維護一個未連線佇列,該佇列為每個客戶端的SYN包(syn=j)開設一個條目,該條目表明伺服器已收到SYN包,

並向客戶發出確認,正在等待客戶的確認包。這些條目所標識的連線在伺服器處於 Syn_RECV狀態,當伺服器收到客戶的確認包時,

刪除該條目,伺服器進入ESTABLISHED狀態。

未連線佇列:

在三次握手協議中,伺服器維護一個未連線佇列,該佇列為每個客戶端的SYN包(syn=j)開設一個條目,該條目表明伺服器已收到SYN包,

並向客戶發出確認,正在等待客戶的確認包。這些條目所標識的連線在伺服器處於 Syn_RECV狀態,當伺服器收到客戶的確認包時,

刪除該條目,伺服器進入ESTABLISHED狀態。

監聽服務好了後,你可以在任何你高興的時候去檢查未連線佇列,看哪個條目是ESTABLISHED狀態的,你可以刪除這個條目,跟這個3次握手完成的客戶端程序進行通訊,

進行資料的接受與傳送,同樣任何時刻可以終止通訊。3次握手後,佇列裡存的是客戶端的地址資訊以及ESTABLISHED狀態,如果程序不去接受請求,tcp/ip協議棧不會去刪除

條目的,一旦佇列滿了,後面就沒位置放完成三次握手的請求了。完成三次握手錶示建立了連線,這個是相對於tcp/ip協議棧的,並不表示我們的程序就接受了客戶端的通訊

請求,如果你想接受一個客戶端的通訊請求,那麼需要主動去查詢,然後去接受請求,最後才可以通訊。查詢的時候一般都是以阻塞方式進行的,沒人完成3次握手的通訊實體時,

執行緒會被阻塞,一旦該事件完成,程序會繼續執行該執行緒。伺服器在主執行緒中可以被阻塞,在子執行緒中進行資料收發,這樣主執行緒阻塞了,不會影響子

執行緒。阻塞本來是程序的一種狀態,但是現線上程也是可以的,執行緒阻塞並不代表程序就阻塞了。

下面是伺服器程式碼, 格式很固定,就多了個用子執行緒與請求通訊的實體進行通訊。後面再詳細的研究網路方面的程式設計,目的是為我寫的2d回合角色扮演遊戲(方塊世界:圖片就是

方塊,現在畫畫太醜了,時間也不是很多。)提供一個服務端。


#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <pthread.h>
#include <iostream>
#include <stdio.h>

void* msgHandle(void *);

int main (int argc, const char * argv[])
{
    //struct sockaddr_in : Socket address, internet style
    struct sockaddr_in server_addr;
    server_addr.sin_len = sizeof(struct sockaddr_in);
    server_addr.sin_family = AF_INET;//Address families AF_INET網際網路地址簇
    server_addr.sin_port = htons(111332);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero),8);
    
    //建立socket
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);//SOCK_STREAM 有連線
    if (server_socket == -1) {
        perror("socket error");
        return 1;
    }
    
    //繫結socket:將建立的socket繫結到本地的IP地址和埠,此socket是半相關的,只是負責偵聽客戶端的連線請求,並不能用於和客戶端通訊
    int bind_result = bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (bind_result == -1) {
        perror("bind error");
        return 1;
    }
    
    //listen偵聽 第一個引數是套接字,第二個引數為等待接受的連線的佇列的大小,在connect請求過來的時候,
    //完成三次握手後先將連線放到這個佇列中,直到被accept處理。如果這個佇列滿了,且有新的連線的時候,對方可能會收到出錯資訊。
    if (listen(server_socket, 5) == -1) {
        perror("listen error");
        return 1;
    }

    while (true) {
        // 接受一個客戶端的tcp連線 建立socket跟客戶端通訊
        printf("wait client\n");
        
        struct sockaddr_in client_address;
        socklen_t address_len;
        
        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &address_len);
        setsockopt(client_socket, SOL_SOCKET, SO_NOSIGPIPE, NULL, sizeof(int));
        
        if (client_socket == -1) {
            perror("accept error");
            return -1;
        }
        printf("accept client\n");
        
        // 啟動一個執行緒跟客戶端進行通訊
        pthread_t id;
        pthread_create(&id, NULL, msgHandle, &client_socket);
//        pthread_join(id, NULL);
        pthread_detach(id);
    }
    
    return 0;
}

void* msgHandle(void *arg){
    int client_socket = *(int*)arg;

    printf("client socket id:%d", client_socket);

    int i = 1;
    while (i< 10) {
        printf("%d", i);
        i+=1;
    }
    
    fflush(stdout);
    
    // 進行訊息處理  接受客戶端訊息 然後處理訊息
    
    return 0;
}