1. 程式人生 > >socket的長連線、短連線、半包、粘包與分包

socket的長連線、短連線、半包、粘包與分包

長連線和短連線

長短連線只是一個概念問題,長短連線的socket都是使用普通的socket函式,沒有什麼特殊的。

        長連線是客戶和伺服器建立連線後不斷開,持續用這個連線通訊,持續過程中一般需要連線偵測,客戶探測服務,或者服務心跳告知客戶,應用層的保活機制。

        短連線是通訊一次後就關閉連線。長短連線是一種通訊約定,需要雙方一起遵守。比如在長連線時,兩端都不close,客戶端/服務端協議保活;短連線時兩端都要主動或被動close,以完成四路釋放。

如果惡意客戶端就是不close怎麼辦?
例如伺服器主動關閉,進入FIN_WAIT_1狀態,這時客戶端有三種情況:

  • ① 如果客戶端機器崩潰導致沒有ACK響應,重傳一定次數後直接回收連線。
  • ② 如果客戶端程序崩潰,客戶端回送RST分節,伺服器端收到RST分節後直接回收連線
  • ③ 如果客戶端正常,則客戶端回送ACK,伺服器進入FIN_WAIT_2狀態,等待客戶端close,如果伺服器執行了close全關閉,而客戶端一直不傳送FIN,則伺服器等待10分75秒將進入CLOSED狀態,連線被完全釋放,此後客戶端再close將收到RST響應。
最好的解決方案是伺服器端設定SO_LINGER,當伺服器執行close時就直接回收連線,傳送RST分節給客戶端。
什麼情況下發送RST分節(復位報文段)
收到RST的連線將被系統回收,再次讀寫套接字埠則觸發訊號SIGPIPE,預設操作是終止程序。
  • ① 客戶端請求一個未監聽的伺服器埠,則伺服器返回RST分節
  • ② 主動異常終止一個連線。(不用4路握手釋放),設定SO_LINGER選項
  • ③ 檢測半開啟連線,當連線一端的主機崩潰並重啟後,如果另一端傳送資料,以RST響應。
  • ④ 當監聽套接字關閉時,對監聽已完成佇列中的連線的對端都發送RST

什麼是半連線和半關閉?
半連線是指已經建立好的連線的一端已經關閉或異常終止,而另一端卻不知道(依然顯示連線ESTABLISHED)的狀況;半關閉是指已建立連線的一端執行半關閉,終止某個方向的資料傳送,這時連線處於半關閉狀態(主動關閉端FIN_WAIT_2,另一端CLOSE_WAIT)。

長連線和短連線概念:  

短連線

短連線:顧名思義,與長連線的區別就是,客戶端收到服務端的響應後,立刻傳送FIN訊息,主動釋放連線。也有服務端主動斷連的情況,凡是在一次訊息互動(發請求-收響應)之後立刻斷開連線的情況都稱為短連線。注:短連線是建立在TCP協議上的,有完整的握手揮手流程,區別於UDP協議。
短連線過程:
連線->傳輸資料->關閉連線-> ...... -> 連線->傳輸資料->關閉連線 -> ...... ->連線->傳輸資料->關閉連線

也可以這樣說:短連線是指SOCKET連線後傳送後接收完資料後馬上斷開連線。
HTTP是無狀態的,瀏覽器和伺服器每進行一次HTTP操作,就建立一次連線,但任務結束就中斷連線。 

HTTP協議是基於請求/響應模式的,因此只要服務端給了響應,本次HTTP連線就結束了。或者更準確的說,是本次HTTP請求就結束了,根本沒有長連線這一說。那麼自然也就沒有短連線這一說了。之所以說HTTP分為長連線和短連線,其實本質上是說的TCP連線。TCP連線是一個雙向的通道,它是可以保持一段時間不關閉的,因此TCP連線才有真正的長連線和短連線這一說。

TCP短連線的情況:

client 向 server 發起連線請求
server 接到請求予以相應和確認,雙方建立連線
client 向 server 傳送訊息
server 迴應 client
一次讀寫完成,此時雙方任何一個都可以發起 close 操作,一般都是 client 先發起 close

短連線通訊模式


長連線

長連線:系統通訊連線建立後就一直保持。長連線也叫持久連線,在TCP層握手成功後,不立即斷開連線,並在此連線的基礎上進行多次訊息(包括心跳)互動,直至連線的任意一方(客戶端OR服務端)主動斷開連線,此過程稱為一次完整的長連線。長連線意味著連線會被複用。既然長連線是指的TCP連線,也就是說複用的是TCP連線。當多個HTTP請求可以複用同一個TCP連線,這就節省了很多TCP連線建立和斷開的消耗。HTTP 1.1相對於1.0最重要的新特性就是引入了長連線。

長連線過程:連線(只建立一次連線) -> 傳輸資料 -> 保持連線 -> ...... -> 傳輸資料 -> 保持連線 -> 關閉連線(只有一次關閉)。
長連線指建立SOCKET連線後不管是否使用都保持連線,但安全性較差。其實長連線是相對於通常的短連線而說的,也就是長時間保持客戶端與服務端的連線狀態。這就要求長連線在沒有資料通訊時,定時傳送資料包,以維持連線狀態,這就是心跳機制。短連線在沒有資料傳輸時直接關閉就行了。

場景:比如你請求了CSDN的一個網頁,這個網頁裡肯定還包含了CSS、JS等等一系列資源,如果你是短連線(也就是每次都要重新建立TCP連線)的話,那你每開啟一個網頁,基本要建立幾個甚至幾十個TCP連線,這就浪費了很多資源。但如果是長連線的話,那麼這麼多次HTTP請求(這些請求包括請求網頁內容,CSS檔案,JS檔案,圖片等等),其實使用的都是一個TCP連線,很顯然是可以節省很多資源。最後關於長連線還要多提一句,那就是,長連線並不是永久連線的。如果一段時間內(具體的時間長短,是可以在header當中進行設定的,也就是所謂的超時時間),這個連線沒有HTTP請求發出的話,那麼這個長連線就會被斷掉。這一點其實很容易理解,否則的話,TCP連線將會越來越多,直到把伺服器的TCP連線數量撐爆到上限為止。現在想想,對於伺服器來說,伺服器裡的這些個長連線其實很有資料庫連線池的味道,都是為了節省連線並重複利用。

TCP長連線的操作流程:

client 向 server 發起連線
一次讀寫完成,連線不關閉
後續讀寫操作...

長連線通訊模式


長連線:client方與server方先建立連線,連線建立後不斷開,然後再進行報文傳送和接收。這種方式下由於通訊連線一直存在。此種方式常用於P2P通訊。

短連線:Client方與server每進行一次報文收發交易時才進行通訊連線,交易完畢後立即斷開連線。此方式常用於一點對多點通訊。C/S通訊。

如何快速區分當前連線使用的是長連線還是短連線

  1. 凡是在一次完整的訊息互動(發請求-收響應)之後,立刻斷開連線(有一方傳送FIN訊息)的情況都稱為短連線;
  2. 長連線的一個明顯特徵是會有心跳訊息(也有沒有心跳的情況),且一般心跳間隔都在30S或者1MIN左右,用wireshark抓包可以看到有規律的心跳訊息互動(可能會存在毫秒級別的誤差)。

長連線與短連線的使用時機

        長連線多用於操作頻繁,點對點的通訊,而且連線數不能太多的情況。每個TCP連線的建立都需要三次握手,每個TCP連線的斷開要四次握手。如果每次操作都要建立連線然後再操作的話處理速度會降低,所以每次操作下次操作時直接傳送資料就可以了,不用再建立TCP連線。例如:資料庫的連線用長連線,如果用短連線頻繁的通訊會造成socket錯誤,頻繁的socket建立也是對資源的浪費。

        短連線:web網站的http服務一般都用短連線。因為長連線對於伺服器來說要耗費一定的資源。像web網站這麼頻繁的成千上萬甚至上億客戶端的連線用短連線更省一些資源。試想如果都用長連線,而且同時用成千上萬的使用者,每個使用者都佔有一個連線的話,可想而知伺服器的壓力有多大。所以併發量大,但是每個使用者又不需頻繁操作的情況下需要短連線。

總之:長連線和短連線的選擇要視需求而定。

長連線的心跳機制


        通訊實體間使用長連線時,一般還需要定義心跳訊息,定期傳送來檢測系統間鏈路是否異常,每隔一定時間傳送一次心跳,如果一定次數沒有收到心跳訊息,這認為此連接出現問題,需要斷開連線重新建立。具體心跳訊息的格式,以及傳送間隔,以及多少次沒有收到心跳就認為鏈路異常,以及資料部是否算作心跳訊息(有的系統如果接收到資料包則會清除心跳計時器也就相當於系統中的資料包也算作心跳訊息);這個需要兩端進行協商。比如GSM常用的短訊息中心和其他網路實體互連的SMPP協議,要求建立的就是長連線.

        心跳包之所以叫心跳包是因為:它像心跳一樣每隔固定時間發一次,以此來告訴伺服器,這個客戶端還活著。事實上這是為了保持長連線,至於這個包的內容,是沒有什麼特別規定的,不過一般都是很小的包,或者只包含包頭的一個空包。

在TCP的機制裡面,本身是存在有心跳包的機制的,也就是TCP的選項:SO_KEEPALIVE。系統預設是設定的2小時的心跳頻率。但是它檢查不到機器斷電、網線拔出、防火牆這些斷線。而且邏輯層處理斷線可能也不是那麼好處理。一般,如果只是用於保活還是可以的。
心跳包一般來說都是在邏輯層傳送空的echo包來實現的。下一個定時器,在一定時間間隔下發送一個空包給客戶端,然後客戶端反饋一個同樣的空包回來,伺服器如果在一定時間內收不到客戶端傳送過來的反饋包,那就只有認定說掉線了。
其實,要判定掉線,只需要send或者recv一下,如果結果為零,則為掉線。但是,在長連線下,有可能很長一段時間都沒有資料往來。理論上說,這個連線是一直保持連線的,但是實際情況中,如果中間節點出現什麼故障是難以知道的。更要命的是,有的節點(防火牆)會自動把一定時間之內沒有資料互動的連線給斷掉。在這個時候,就需要我們的心跳包了,用於維持長連線,保活。

在獲知了斷線之後,伺服器邏輯可能需要做一些事情,比如斷線後的資料清理呀,重新連線呀……當然,這個自然是要由邏輯層根據需求去做了。

TCP的socket本身就是長連線,為什麼還需要心跳包?

  1. 內網機器如果不主動向外發起連線,外網機沒法直接連線內網的,這也是內網機安全的原因之一,又因為路由器會把這個關係記錄下來,但是過一段時間這個紀錄可能會丟失,所以每一個客戶端每個一定時間就會向伺服器傳送訊息,以確保伺服器可以隨時找到你,這個東西被稱為心跳包。
  2. 理論上說,這個連線事一直保持連線的,但實際情況中,如果中間出現什麼情況是難以想象的。更要命的是,有的節點(防火牆)會自動把一定時間之內沒有資料互動的連線給斷掉。這個時候我們就需要心跳包了,用於維持長連線,保活。

總之:心跳包主要也就是用於長連線的保活和斷線處理。一般的應用下,判定時間在30-40秒比較不錯。如果實在要求高,那就在6-9秒。在TCP socket心跳機制中,心跳包可以由伺服器傳送給客戶端,也可以由客戶端傳送給伺服器,不過比較起來,前者開銷可能更大。

一個由客戶端給伺服器傳送心跳包,基本思路是:

  1. 伺服器為每個客戶端儲存了IP和計數器count,即map<fd, pair<ip, count>>。服務端主執行緒採用 select 實現多路IO複用監聽新連線以及接受資料包(心跳包),子執行緒用於檢測心跳:
            如果主執行緒接收到的是心跳包,將該客戶端對應的計數器 count 清零;
            在子執行緒中,每隔3秒遍歷一次所有客戶端的計數器 count:
                     若 count 小於 5,將 count 計數器加 1;
                    若 count 等於 5,說明已經15秒未收到該使用者心跳包,判定該使用者已經掉線;

  2. 客戶端則只是開闢子執行緒,定時給伺服器傳送心跳包(本示例中定時時間為3秒)。
/*************************************************************************
    > File Name: Server.cpp
    > Author: SongLee
    > E-mail: [email protected]
    > Created Time: 2016年05月05日 星期四 22時50分23秒
    > Personal Blog: http://songlee24.github.io/
************************************************************************/
 
#include<netinet/in.h>   // sockaddr_in
#include<sys/types.h>    // socket
#include<sys/socket.h>   // socket
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/select.h>   // select
#include<sys/ioctl.h>
#include<sys/time.h>
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<cstdlib>
#include<cstdio>
#include<cstring>
using namespace std;
#define BUFFER_SIZE 1024

enum Type {HEART, OTHER};

struct PACKET_HEAD
{
    Type type;
    int length;
};

void* heart_handler(void* arg);

class Server
{
private:
    struct sockaddr_in server_addr;
    socklen_t server_addr_len;
    int listen_fd;        // 監聽的fd
    int max_fd;           // 最大的fd
    fd_set master_set;    // 所有fd集合,包括監聽fd和客戶端fd   
    fd_set working_set;   // 工作集合
    struct timeval timeout;
    map<int, pair<string, int> > mmap;   // 記錄連線的客戶端fd--><ip, count>
public:
    Server(int port);
    ~Server();
    void Bind();
    void Listen(int queue_len = 20);
    void Accept();
    void Run();
    void Recv(int nums);
    friend void* heart_handler(void* arg);
};

Server::Server(int port)
{
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htons(INADDR_ANY);
    server_addr.sin_port = htons(port);
    // create socket to listen
    listen_fd = socket(PF_INET, SOCK_STREAM, 0);
    if(listen_fd < 0)
    {
        cout << "Create Socket Failed!";
        exit(1);
    }
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}

Server::~Server()
{
    for(int fd=0; fd<=max_fd; ++fd)
    {
        if(FD_ISSET(fd, &master_set))
        {
            close(fd);
        }
    }
}

void Server::Bind()
{
    if(-1 == (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))))
    {
        cout << "Server Bind Failed!";
        exit(1);
    }
    cout << "Bind Successfully.\n"; 
}

void Server::Listen(int queue_len)
{
    if(-1 == listen(listen_fd, queue_len))
    {
        cout << "Server Listen Failed!";
        exit(1);
    }
    cout << "Listen Successfully.\n";
}

void Server::Accept()
{
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    int new_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if(new_fd < 0)
    {
        cout << "Server Accept Failed!";
        exit(1);
    }

    string ip(inet_ntoa(client_addr.sin_addr));    // 獲取客戶端IP
    cout << ip << " new connection was accepted.\n";
    mmap.insert(make_pair(new_fd, make_pair(ip, 0)));

    // 將新建立的連線的fd加入master_set
    FD_SET(new_fd, &master_set);
    if(new_fd > max_fd)
    {
        max_fd = new_fd;
    }
}   

void Server::Recv(int nums)
{
    for(int fd=0; fd<=max_fd; ++fd)
    {
        if(FD_ISSET(fd, &working_set))
        {
            bool close_conn = false;  // 標記當前連線是否斷開了

            PACKET_HEAD head;
            recv(fd, &head, sizeof(head), 0);   // 先接受包頭

            if(head.type == HEART)
            {
                mmap[fd].second = 0;        // 每次收到心跳包,count置0
                cout << "Received heart-beat from client.\n";
            }
            else
            {
                // 資料包,通過head.length確認資料包長度 
            }   

            if(close_conn)  // 當前這個連線有問題,關閉它
            {
                close(fd);
                FD_CLR(fd, &master_set);
                if(fd == max_fd)  // 需要更新max_fd;
                {
                    while(FD_ISSET(max_fd, &master_set) == false)
                        --max_fd;
                }
            }
        }
    }   
}

void Server::Run()
{
    pthread_t id;     // 建立心跳檢測執行緒
    int ret = pthread_create(&id, NULL, heart_handler, (void*)this);
    if(ret != 0)
    {
        cout << "Can not create heart-beat checking thread.\n";
    }

    max_fd = listen_fd;   // 初始化max_fd
    FD_ZERO(&master_set);
    FD_SET(listen_fd, &master_set);  // 新增監聽fd

    while(1)
    {
        FD_ZERO(&working_set);
        memcpy(&working_set, &master_set, sizeof(master_set));

        timeout.tv_sec = 30;
        timeout.tv_usec = 0;

        int nums = select(max_fd+1, &working_set, NULL, NULL, &timeout);
        if(nums < 0)
        {
            cout << "select() error!";
            exit(1);
        }

        if(nums == 0)
        {
            //cout << "select() is timeout!";
            continue;
        }

        if(FD_ISSET(listen_fd, &working_set))
            Accept();   // 有新的客戶端請求
        else
            Recv(nums); // 接收客戶端的訊息
    }
}


// thread function
void* heart_handler(void* arg)
{
    cout << "The heartbeat checking thread started.\n";
    Server* s = (Server*)arg;
    while(1)
    {
        map<int, pair<string, int> >::iterator it = s->mmap.begin();
        for( ; it!=s->mmap.end(); )
        {   
            if(it->second.second == 5)   // 3s*5沒有收到心跳包,判定客戶端掉線
            {
                cout << "The client " << it->second.first << " has be offline.\n";

                int fd = it->first;
                close(fd);            // 關閉該連線
                FD_CLR(fd, &s->master_set);
                if(fd == s->max_fd)      // 需要更新max_fd;
                {
                    while(FD_ISSET(s->max_fd, &s->master_set) == false)
                        s->max_fd--;
                }

                s->mmap.erase(it++);  // 從map中移除該記錄
            }
            else if(it->second.second < 5 && it->second.second >= 0)
            {
                it->second.second += 1;
                ++it;
            }
            else
            {
                ++it;
            }
        }
        sleep(3);   // 定時三秒
    }
}

int main()
{
    Server server(15000);
    server.Bind();
    server.Listen();
    server.Run();
    return 0;
}

客戶端程式碼:

/*************************************************************************
    > File Name: Client.cpp
    > Author: SongLee
    > E-mail: [email protected]
    > Created Time: 2016年05月05日 星期四 23時41分56秒
    > Personal Blog: http://songlee24.github.io/
************************************************************************/

#include<netinet/in.h>   // sockaddr_in
#include<sys/types.h>    // socket
#include<sys/socket.h>   // socket
#include<arpa/inet.h>
#include<sys/ioctl.h>
#include<unistd.h>
#include<iostream>
#include<string>
#include<cstdlib>
#include<cstdio>
#include<cstring>
using namespace std;
#define BUFFER_SIZE 1024

enum Type {HEART, OTHER};

struct PACKET_HEAD
{
    Type type;
    int length;
};

void* send_heart(void* arg); 

class Client 
{
private:
    struct sockaddr_in server_addr;
    socklen_t server_addr_len;
    int fd;
public:
    Client(string ip, int port);
    ~Client();
    void Connect();
    void Run();
    friend void* send_heart(void* arg); 
};

Client::Client(string ip, int port)
{
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    if(inet_pton(AF_INET, ip.c_str(), &server_addr.sin_addr) == 0)
    {
        cout << "Server IP Address Error!";
        exit(1);
    }
    server_addr.sin_port = htons(port);
    server_addr_len = sizeof(server_addr);
    // create socket
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0)
    {
        cout << "Create Socket Failed!";
        exit(1);
    }
}

Client::~Client()
{
    close(fd);
}

void Client::Connect()
{
    cout << "Connecting......" << endl;
    if(connect(fd, (struct sockaddr*)&server_addr, server_addr_len) < 0)
    {
        cout << "Can not Connect to Server IP!";
        exit(1);
    }
    cout << "Connect to Server successfully." << endl;
}

void Client::Run()
{
    pthread_t id;
    int ret = pthread_create(&id, NULL, send_heart, (void*)this);
    if(ret != 0)
    {
        cout << "Can not create thread!";
        exit(1);
    }
}

// thread function
void* send_heart(void* arg)
{
    cout << "The heartbeat sending thread started.\n";
    Client* c = (Client*)arg;
    int count = 0;  // 測試
    while(1) 
    {
        PACKET_HEAD head;
        head.type = HEART;
        head.length = 0;    
        send(c->fd, &head, sizeof(head), 0);
        sleep(3);     // 定時3秒

        ++count;      // 測試:傳送15次心跳包就停止傳送
        if(count > 15)
            break;
    }
}

int main()
{
    Client client("127.0.0.1", 15000);
    client.Connect();
    client.Run();
    while(1)
    {
        string msg;
        getline(cin, msg);
        if(msg == "exit")
            break;
        cout << "msg\n";
    }
    return 0;
}


可以看出,客戶端啟動以後傳送了15次心跳包,然後停止傳送心跳包。在經過一段時間後(3s*5),伺服器就判斷該客戶端掉線,並斷開了連線。

長輪詢和短輪詢

保護訊息邊界和流

保護訊息邊界,就是指傳輸協議把資料當作一條獨立的訊息在網上傳輸,接收端只能接收獨立的訊息。也就是說存在保護訊息邊界,接收端一次只能接收發送端發出的一個數據包。而面向流則是指無保護訊息保護邊界的,如果傳送端連續傳送資料,接收端有可能在一次接收動作中,會接收兩個或者更多的資料包。
例如,我們連續傳送三個資料包,大小分別是2k,4k ,8k,這三個資料包,都已經到達了接收端的網路堆疊中,如果使用UDP協議,不管我們使用多大的接收緩衝區去接收資料,我們必須有三次接收動作,才能夠把所有的資料包接收完.而使用TCP協議,我們只要把接收的緩衝區大小設定在14k以上,我們就能夠一次把所有的資料包接收下來,只需要有一次接收動作。
        注意:這就是因為UDP協議的保護訊息邊界使得每一個訊息都是獨立的。而流傳輸卻把資料當作一串資料流,他不認為資料是一個一個的訊息。所以有很多人在使用tcp協議通訊的時候,並不清楚tcp是基於流的傳輸,當連續傳送資料的時候,他們時常會認識tcp會丟包。其實不然,因為當他們使用的緩衝區足夠大時,他們有可能會一次接收到兩個甚至更多的資料包,而很多人往往會忽視這一點,只解析檢查了第一個資料包,而已經接收的其他資料包卻被忽略了。所以大家如果要作這類的網路程式設計的時候,必須要注意這一點。

Client 和 Server 通訊傳送與接收方式設計

在長連線中一般是沒有條件能夠判斷讀寫什麼時候結束,所以必須要加長度報文頭。讀函式先是讀取報文頭的長度,再根據這個長度去讀相應長度的報文。

在通訊資料傳送與接收之間也存在不同的方式,即同步和非同步兩種方式。這裡的同步和非同步與 I/O 層次的同異步概念不同。主要涉及 socket APIs recv() 和 send() 的不同組合方式。

同步傳送與接收

從應用程式設計的角度講,報文傳送和接收是同步進行的,既報文傳送後,傳送方等待接收方返回訊息報文。同步方式一般需要考慮超時問題,即報文發出去後傳送 方不能無限等待,需要設定超時時間,超過該時間後傳送方不再處於等待狀態中,而直接被通知超時返回。同步傳送與接收經常與短連線通訊方式結合使用,稱為同 步短連線通訊方式,其 socket 事件流程可如上面的圖 12 所示。

非同步傳送與接收

從應用程式設計的角度講,傳送方只管傳送資料,不需要等待接收任何返回資料,而接收方只管接收資料,這就是應用層的非同步傳送與接收方式。要實現非同步方式, 通常情況下報文傳送和接收是用兩個不同的程序來分別處理的,即傳送與接收是分開的,相互獨立的,互不影響。非同步傳送與接收經常與長連線通訊方式結合使用, 稱為非同步長連線通訊方式。從應用邏輯角度講,這種方式又可分雙工和單工兩種情況。

非同步雙工

非同步雙工是指應用通訊的接收和傳送在同一個程式中,而有兩個不同的子程序分別負責傳送和接收,非同步雙工模式是比較複雜的一種通訊方式,有時候經常會出現在 不同機構之間的兩套系統之間的通訊。比如銀行與銀行之間的資訊交流。它也可以適用在現代 P2P 程式中。如圖 14 所示,Server 和 Client 端分別 fork 出兩個子程序,形成兩對子程序之間的連線,兩個連線都是單向的,一個連線是用於傳送,另一個連線用於接收,這樣方式的連線就被稱為非同步雙工方式連線。

長連線非同步雙工模式


非同步單工
應用通訊的接收和傳送是用兩個不同的程式來完成,這種非同步是利用兩對不同程式依靠應用邏輯來實現的。下面圖顯示了長連線方式下的非同步單工模式,在通訊的 A 和 B 端,分別有兩套 Server 和 Client 程式,B 端的 Client 連線 A 端的 Server,A 端的 Server 只負責接收 B 端 Client 傳送的報文。A 端的 Client 連線 B 端的 Server,A 端 Client 只負責向 B 端 Server 傳送報文。

長連線非同步單工模式


粘包和拆包

之所以出現粘包和半包現象,是因為TCP當中,只有流的概念,沒有包的概念 。

TCP是一種流協議(stream protocol)。這就意味著資料是以位元組流的形式傳遞給接收者的,沒有固有的"報文"或"報文邊界"的概念。從這方面來說,讀取TCP資料就像從串列埠讀取資料一樣--無法預先得知在一次指定的讀呼叫中會返回多少位元組(也就是說能知道總共要讀多少,但是不知道具體某一次讀多少)

看一個例子:我們假設在主機A和主機B的應用程式之間有一條TCP連線,主機A有兩條報文D1,D2要傳送到B主機,並兩次呼叫send來發送,每條報文呼叫一次。


半包
指接受方沒有接受到一個完整的包,只接受了部分,這種情況主要是由於TCP為提高傳輸效率,將一個包分配的足夠大,導致接受方並不能一次接受完。( 在長連線和短連線中都會出現)。  

粘包與分包
指傳送方傳送的若干個包資料到接收方接收時粘成一個完整的包資料,從接收緩衝區看,後一包資料的頭緊接著前一包資料的尾。
如下圖:客戶端同一時間傳送幾條資料,而服務端只能收到一大條資料,


由於傳輸的過程為資料流,經過 TCP 傳輸後,三條資料被合併成了一條,這就是資料粘包了

出現粘包現象的原因是多方面的,它既可能由傳送方造成,也可能由接收方造成。

傳送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,傳送方往往要收集到足夠多的資料後才傳送一包資料。若連續幾次傳送的資料都很少,通常TCP會根據優化演算法把這些資料合成一包後一次傳送出去,這樣接收方就收到了粘包資料。這麼做優點也很明顯,就是為了減少廣域網的小分組數目,從而減小網路擁塞的出現。總的來說就是:傳送端傳送了幾次資料,接收端一次性讀取了所有資料,造成多次傳送一次讀取通常是網路流量優化,把多個小的資料段集滿達到一定的資料量,從而減少網路鏈路中的傳輸次數。

接收方引起的粘包是由於接收方使用者程序不及時接收資料,從而導致粘包現象。這是因為接收方先把收到的資料放在系統接收緩衝區,使用者程序從該緩衝區取資料,若下一包資料到達時前一包資料尚未被使用者程序取走,則下一包資料放到系統接收緩衝區時就接到前一包資料之後,而使用者程序根據預先設定的緩衝區大小從系統接收緩衝區取資料,這樣就一次取到了多包資料。分包是指在出現粘包的時候我們的接收方要進行分包處理。(在長連線中都會出現)。總的來說就是:傳送端傳送了數量比較多的資料,接收端讀取資料時候資料分批到達,造成一次傳送多次讀取通常和網路路由的快取大小有關係,一個數據段大小超過快取大小,那麼就要拆包傳送。

如圖所示:


TCP粘包的解決方案有很多種方法,最簡單的一種就是傳送的資料協議定義傳送的資料包的結構:

  1. 資料頭:資料包的大小,固定長度。
  2. 資料內容:資料內容,長度為資料頭定義的長度大小。
實際操作如下:
  • 傳送端:先發送資料包的大小,再發送資料內容。
  • 接收端:先解析本次資料包的大小N,在讀取N個位元組,這N個位元組就是一個完整的資料內容。

具體流程如下:


初涉socket程式設計的朋友經常有下面一些疑惑:
1. 為什麼我發了3次,另一端只收到2次?
2. 我每次傳送都成功了,為什麼對方收到的資訊不完整?
這些疑惑往往是對send和recv這兩個函式理解不準確所致。send和recv都提供了一個長度引數。
對於send而言,這是你希望傳送的位元組數,而對於recv而言,則是希望收到的最大位元組數。

tcp粘包、半包的處理方式:一是採用分隔符的方式,採用特殊的分隔符作為一個數據包的結尾;二是採用給每個包的特定位置(如包頭兩個位元組)加上資料包的長度資訊,另一端收到資料後根據資料包的長度擷取特定長度的資料解析,假設包頭資訊的資料長度為infoLen,接收到的資料包真實長度為trueLen,那麼有如下幾種情況:

  • 1: infoLen>trueLen,半包。
  • 2: infoLen<trueLen,粘包。
  • 3: infoLen=trueLen,正常。

實現原始碼

/**  
 * read size of len from sock into buf.  
 */    
bool readPack(int sock, char* buf, size_t len) 
{    
    if (NULL == buf || len < 1) 
    {
        return false;    
    }    
    memset(buf, 0, len); // only reset buffer len.    
    ssize_t read_len = 0, readsum = 0;    
    do 
    {    
        read_len = read(sock, buf + readsum, len - readsum);    
        if (-1 == read_len) // ignore error case    
        { 
            return false;    
        }    
        printf("receive data: %s\n", buf + readsum);    
        readsum += read_len;    
    } while (readsum < len && 0 != read_len);    
    return true;    
} 

測試用例介紹:提供的demo主要流程如下:
1. 客戶端負責模擬傳送資料,服務端負責接受資料,處理粘包問題
a)emulate_subpackage
        模擬情況1,一個長資料經過多次才到達目的地,
        在客戶端字串“This is a test case for client send subpackage data. data is not send complete at once.”每次只發送6個位元組長度。服務端要把字串集滿才能處理資料(列印字串)

b)emulate_adheringpackage
        模擬情況2,多個數據在一次性到達目的地
        在客戶端將字串“Hello I'm lucky. Nice too me you”切成三個資料段(都包含資料頭和資料內容),然後一次性發送,服務端讀取資料時對三個資料段逐個處理。

server.cpp

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 12345  // 埠
#define CONN 5      // 接收連線數

void newClient(int sockfd);                        
bool readPack(int sockfd, char* buf, size_t len);  // 讀取資料
void safeAndClose(int &sockfd);

int main()
{
    int sockfd = -1, newsockfd = -1;
    socklen_t len = 0;
    struct sockaddr_in server_addr, client_addr;

    //creat socket
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0))<0)
    {
        printf("Creat socket fail and errno: %d\n", errno);
        exit(errno);
    }
    printf("sockfd : %d\n", sockfd);
    
    bzero(&server_addr, sizeof(sockaddr_in));
    server_addr.sin_family=AF_INET;
    server_addr.sin_addr.s_addr=INADDR_ANY;
    server_addr.sin_port=htons(PORT);
    
    if((bind(sockfd, (sockaddr*)&server_addr, sizeof(server_addr)))<0)
    {
        printf("Bind fail and errno: %d\n", errno);
        exit(errno);
    }

    if((listen(sockfd, CONN))<0)
    {
        printf("Listen fail and errno: %d\n",errno);
        exit(errno);
    }
    printf("start listening...\n");
    len = sizeof(sockaddr_in);
    int i = 0;
    while(i++ < 3)
    {
        printf("start accept\n");
        newsockfd = accept(sockfd, (sockaddr*)&client_addr,&len);
        if(newsockfd<0)
        {
            printf("accept fail and errno: %d\n", errno);
            exit(errno);
        }

        pid_t pid = fork();
        if(0==pid) //子程序
        {
            safeAndClose(sockfd); //子程序關閉監聽的描述符
            newClient(newsockfd);
            break;            
        }
        else if(pid >0) //父程序只接收客戶端連線
        {
            safeAndClose(newsockfd); //父程序關閉和客戶端通訊的描述符
        }
    }
    safeAndClose(sockfd);
    return 0;
}

void newClient(int sockfd)
{
    printf("new client socket fd: %d\n", sockfd);
    int dataSize=0;
    const int HEAD_SIZE=9; //頭部大小
    char buf[512] = {0};
    for(;;)
    {
        memset(buf, 0, sizeof(buf));

        //read head 
        if(!readPack(sockfd, buf, HEAD_SIZE))
        {
            printf("read head buf fail\n");
            safeAndClose(sockfd);
            return;
        }
        dataSize = atoi(buf);//頭部資料裡面儲存要接收收據長度
        printf("data size: %s, value:%d\n", buf, dataSize);
        memset(buf, 0, sizeof(buf));
        if(!readPack(sockfd, buf, dataSize))
        {
            printf("read data buf fail\n");
            safeAndClose(sockfd);
            return ;
        }
        printf("data size: %d, Text: %s\n", dataSize, buf);
        if(0==strcmp(buf, "exit")) break;
    }
    memset(buf, 0, sizeof(buf));
    snprintf(buf, sizeof(buf), "server -> client : from server read complete.");
    write(sockfd, buf, strlen(buf)+1);
    printf("new client socket fd: %d finish\n", sockfd);
    safeAndClose(sockfd);
}


bool readPack(int sockfd, char* buf, size_t len)
{
    if(NULL==buf || len<1)
    {
        return false;
    }
    memset(buf, 0, len);
    ssize_t read_len = 0, read_sum = 0;
    do{
        read_len = read(sockfd, buf+read_sum, len-read_sum);
        if(read_len == -1) return false;
        printf("receive data : %s\n",buf+read_sum);
        read_sum += read_len;
    }while(read_sum<len && 0 !=read_len);
    return true;
}

void safeAndClose(int &sockfd)
{
    if(sockfd > 0)
    {
        close(sockfd);
        sockfd = -1;
    }
}

client.cpp

#include <cstdio>    
#include <cstdlib>    
#include <cstring>    
#include <time.h>    
#include <errno.h>    
#include <sys/socket.h>    
#include <arpa/inet.h>    
#include <unistd.h>    

#define PORT 12345
    
void safe_close(int &sock);    
void emulate_subpackage(int sock);    
void emulate_adheringpackage(int sock);    
    
int main(int argc, char *argv[])   
{    
    char buf[128] = {0};    
    int sockfd = -1;    
    struct sockaddr_in serv_addr;    
    
    // Create sock    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);    
    if (-1 == sockfd)   
    {    
        printf("new socket failed. errno: %d, error: %s\n", errno, strerror(errno));    
        exit(-1);    
    }    
    
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");    
    serv_addr.sin_family = AF_INET;    
    serv_addr.sin_port = htons(PORT);    
    
    // Connect to remote server    
    if (connect(sockfd, (sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)   
    {    
        printf("connection failed. errno: %d, error: %s\n", errno, strerror(errno));    
        exit(-1);    
    }    
    emulate_subpackage(sockfd);    
    emulate_adheringpackage(sockfd);    
    
    const int HEAD_SIZE = 9;    
    const char temp[] = "exit";    
    memset(buf, 0, sizeof(buf));    
    snprintf(buf, sizeof(buf), "%0.*zu", HEAD_SIZE - 1, sizeof(temp));    
    write(sockfd, buf, HEAD_SIZE);    
    write(sockfd, temp, sizeof(temp));    
    
    printf("send complete.\n");    
    memset(buf, 0, sizeof(buf));    
    read(sockfd, buf, sizeof(buf));    
    printf("receive data: %s\n", buf);    
    printf("client finish.\n");    
    
    safe_close(sockfd);    
    return 0;    
}    
    
void safe_close(int &sock)   
{    
    if (sock > 0) {    
        close(sock);    
        sock = -1;    
    }    
}    
    
/**  
 * emulate socket data write multi part.  
 */    
void emulate_subpackage(int sock)   
{    
    printf("emulate_subpackage...\n");    
    char text[] = "This is a test case for client send subpackage data. data is not send complete at once.";    
    const size_t TEXTSIZE = sizeof(text);    
    ssize_t len = 0;    
    size_t sendsize = 0, sendsum = 0;    
    
    const int HEAD_SIZE = 9;    
    char buf[64] = {0};    
    snprintf(buf, HEAD_SIZE, "%08zu", TEXTSIZE);    
    write(sock, buf, HEAD_SIZE);    
    printf("send data size: %s\n", buf);    
    
    do   
    {    
        sendsize = 6;    
        if (sendsum + sendsize > TEXTSIZE)   
        {    
            sendsize = TEXTSIZE - sendsum;    
        }    
        len = write(sock, text + sendsum, sendsize);    
        if (-1 == len)   
        {    
            printf("send data failed. errno: %d, error: %s\n", errno, strerror(errno));    
            return;    
        }    
        memset(buf, 0, sizeof(buf));    
        snprintf(buf, len + 1, text + sendsum);    
        printf("send data: %s\n", buf);    
        sendsum += len;    
        sleep(1);    
    } while (sendsum < TEXTSIZE && 0 != len);    
}    
    
/**  
 * emualte socket data write adhering.  
 */    
void emulate_adheringpackage(int sock)   
{    
    printf("emulate_adheringpackage...\n");    
    const int HEAD_SIZE = 9;    
    char buf[1024] = {0};    
    char text[128] = {0};    
    char *pstart = buf;    
    
    // append text    
    memset(text, 0, sizeof(text));    
    snprintf(text, sizeof(text), "Hello ");    
    snprintf(pstart, HEAD_SIZE, "%08zu", strlen(text) + 1);    
    pstart += HEAD_SIZE;    
    snprintf(pstart, strlen(text) + 1, "%s", text);    
    pstart += strlen(text) + 1;    
    
    // append text    
    memset(text, 0, sizeof(text));    
    snprintf(text, sizeof(text), "I'm lucky.");    
    snprintf(pstart, HEAD_SIZE, "%08zu", strlen(text) + 1);    
    pstart += HEAD_SIZE;    
    snprintf(pstart, strlen(text) + 1, "%s", text);    
    pstart += strlen(text) + 1;    
    
    // append text    
    memset(text, 0, sizeof(text));    
    snprintf(text, sizeof(text), "Nice too me you");    
    snprintf(pstart, HEAD_SIZE, "%08zu", strlen(text) + 1);    
    pstart += HEAD_SIZE;    
    snprintf(pstart, strlen(text) + 1, "%s", text);    
    pstart += strlen(text) + 1;    
    write(sock, buf, pstart - buf);    
} 

相關推薦

socket連線連線分包

長連線和短連線 長短連線只是一個概念問題,長短連線的socket都是使用普通的socket函式,沒有什麼特殊的。         長連線是客戶和伺服器建立連線後不斷開,持續用這個連線通訊,持續過程中一般需要連線偵測,客戶探測服務,或者服務心跳告知客戶

連線 連線心跳機制斷線重連(轉載) Socket連線連線

概述 可承遇到,不知什麼原因,一個夜晚,機房中,大片的遠端呼叫連線斷開。 第二天早上,使用者訪問高峰,大部分伺服器都在獲取連線,造成大片網路阻塞。 服務崩潰,慘不忍睹的景象。 本文將從長連線和短連線的概念切入,再到長連線與短連線的區別,以及應用場景,引出心跳機制和斷線重連,給出程式碼實現。 從原

什麼是socket?什麼是socket連線?java如何簡單實現socket客戶端和伺服器?

*socket就是套接字,是一種通訊方式!採用這種方式可以實現客戶端和伺服器之間的通訊! 百度百科的解釋:        Socket的英文原義是“孔”或“插座”。作為BSD UNIX的程序通訊機制,取後一種意思。通常也稱作"套接字",用於描述IP地址和埠,是一個通訊鏈的控

[Golang] 從零開始寫Socket Server(3): 對連線的處理策略(模擬心跳)

    通過前兩章,我們成功是寫出了一套湊合能用的Server和Client,並在二者之間實現了通過協議交流。這麼一來,一個簡易的socket通訊框架已經初具雛形了,那麼我們接下來做的,就是想辦法讓這

連線連線輪詢和WebSocket

對這四個概念不太清楚,今天專門搜尋瞭解一下,總結一下: 長連線:在HTTP 1.1,客戶端發出請求,服務端接收請求,雙方建立連線,在服務端沒有返回之前保持連線,當客戶端再發送請求時,它會使用同一個連線。這一直繼續到客戶端或伺服器端認為會話已經結束,其中一方中斷連

tomcat連線連線配置及用途

1.WEB應用有很多,下面就兩個典型的應用(管理頁面和介面服務)做對比。     管理頁面:多涉及到使用者的登入和長時間的頻繁操作處理,這些操作都集中在一個session中,建議採用長連線;     介面服務:比如常見的webservice,操作集中在很短時間內完成,

談談HTTP協議中的輪詢輪詢連線連線

---------------------  作者:左瀟龍  來源:CSDN  原文:https://blog.csdn.net/zuoxiaolong8810/article/details/65441709  版權宣告:本文為博主原創文章,轉載請附上博文連結!

HTTP連線連線究竟是什麼?

1. HTTP協議與TCP/IP協議的關係 HTTP的長連線和短連線本質上是TCP長連線和短連線。HTTP屬於應用層協議,在傳輸層使用TCP協議,在網路層使用IP協議。 IP協議主要解決網路路由和定址問題,TCP協議主要解決如何在IP層之上可靠地傳遞資料包,使得網路上接收端

Http連線連線持久連線這三個概念的分析總結

什麼是Http長連線 長連線定義: client方與server方先建立連線,連線建立後不斷開,然後再進行報文傳送和接收。這種方式下由於通訊連線一直存在。此種方式常用於P2P點對點的通訊。 長連線的操作步驟是:建立連線——資料傳輸...(保持連線)...資料傳輸——關閉

雜談——HTTP連線連線以及長短輪詢

1.什麼是長連線、短連線?   在HTTP/1.0中,預設使用的是短連線。也就是說,瀏覽器和伺服器每進行一次HTTP操作,就建立一次連線,任務結束就中斷連線。如果客戶端瀏覽器訪問的某個HTML或其他型別的 Web頁中包含有其他的Web資源,如JavaScript檔案、影象檔案、CSS檔案等,每遇

JAVA之連線連線和心跳

短連線: client向server發起連線,server接到請求,雙方建立連線,client向server傳送訊息,server迴應client,一次讀寫完成雙方都可以發起close請求 優點:短連線對於伺服器來說較為簡單,存在的連線都是有用的連線,不需要額外的控制。

基礎知識概念(1):Socket 連線連線的概念

1.短連線 連線->傳輸資料->關閉連線     HTTP是無狀態的,瀏覽器和伺服器每進行一次HTTP操作,就建立一次連線,但任務結束後就中斷連線。短連線是指SOCKET建立連線後 ,傳送後或接收完資料後,就馬上斷開連線。 2.長連線

Socket連線連線的區別

TCP/IP TCP/IP是個協議組,可分為三個層次:網路層、傳輸層和應用層。 在網路層有IP協議、ICMP協議、ARP協議、RARP協議和BOOTP協議。 在傳輸層中有TCP協議與UDP協議。 在應用層有:TCP包括FTP、HTTP、TELNET、SMTP等協議 UDP包括DNS

Java網路程式設計(一) TCP/IP,http,socket連線連線

TCP/IP  TCP/IP是個協議組,可分為三個層次:網路層、傳輸層和應用層。  在網路層有IP協議、ICMP協議、ARP協議、RARP協議和BOOTP協議。  在傳輸層中有TCP協議與UDP協議。  在應用層有:TCP包括FTP、HTTP、TELNET、SMTP等協議

socket中的連線連線淺析

socket中的長連線和短連線 長連線和短連線 當網路通訊時採用TCP協議時,需在通訊雙方間建立連線,當讀寫操作完成後不再需要這個連線時就可以釋放這個連線。 所謂的短連線就是通訊雙方建立一個TCP連線,完成資料傳送後即斷開此連線。 長連線是針對短連結

Socket連線連線(很詳細)

長連線與短連線 所謂長連線,指在一個TCP連線上可以連續傳送多個數據包,在TCP連線保持期間,如果沒有資料包傳送,需要雙方發檢測包以維持此連線,一般需要自己做線上維持。  短連線是指通訊雙方有資料互動時,就建立一個TCP連線,資料傳送完成後,則斷開此TCP連線,一般銀行都使

什麼是連線連線(不看後悔,一看必懂)

在日常專案中,大多的時候我們用的是短連線,一個請求過來,一個執行緒處理完該請求,執行緒被執行緒池回收,這個請求就關閉了.雖然這能滿足很大部分的需求,但是也有些問題,比如說:如果客戶端發的請求比較多,比較頻繁,服務端就會忙於建立連線處理請求,由於服務端的執行緒數也有限,併發比較大的話有可能會造成服

連線連線區別和優缺點

TCP與UDP  udp:面向無連線的通訊協議,資料包括目的埠資訊和源埠資訊 優點:面向無連線,操作簡單,要求系統資源較少,速度快,由於不需要連線,可進行廣播發送 缺點:傳送資料之前不需要與對方建立連線,接收到資料時也不需要傳送確認訊號,傳送端不知道接收端是否正確接接收,不會重

tcp的連線連線

tcp長連線和短連線 TCP在真正的讀寫操作之前,server與client之間必須建立一個連線,當讀寫操作完成後,雙方不再需要這個連線時它們可以釋放這個連線,連線的建立通過三次握手,釋放則需要四次握手,所以說每個連線的建立都是需要資源消耗和時間消耗的。 TCP通訊的整個過程,如下圖: 1. TCP

http和Tcp的連線連線

轉自:https://www.cnblogs.com/fubaizhaizhuren/p/7523374.html http協議和tcp/ip 協議的關係 (1) http是應用層協議,tcp協議是傳輸層協議,ip協議是網路協議。 (2) IP協議主要解決網路路由和定址問題 (3)