1. 程式人生 > >TCP自定義通訊協議

TCP自定義通訊協議

我們為什麼要自定義TCP應用層傳輸協議?

因為在TCP流傳輸的過程中,可能會出現分包與黏包的現象。我們為了解決這些問題,需要我們自定義通訊協議進行封包與解包。

什麼是分包與黏包?

分包:指接受方沒有接受到一個完整的包,只接受了部分。
黏包:指傳送方傳送的若干包資料到接收方接收時粘成一包,從接收緩衝區看,後一包資料的頭緊接著前一包資料的尾。
PS:因為TCP是面向位元組流的,是沒有邊界的概念的,嚴格意義上來說,是沒有分包和黏包的概念的,但是為了更好理解,也更好來描述現象,我在這裡就接著採用這兩個名詞來解釋現象了。我覺得大家知道這個概念就行了,不必細扣,能解決問題就行。
產生分包與黏包現象的原因是什麼?

產生分包原因:

可能是IP分片傳輸導致的,也可能是傳輸過程中丟失部分包導致出現的半包,還有可能就是一個包可能被分成了兩次傳輸,在取資料的時候,先取到了一部分(還可能與接收的緩衝區大小有關係),總之就是一個數據包被分成了多次接收。

產生黏包的原因:

由於TCP協議本身的機制(面向連線的可靠地協議-三次握手機制)客戶端與伺服器會維持一個連線(Channel),資料在連線不斷開的情況下,可以持續不斷地將多個數據包發往伺服器,但是如果傳送的網路資料包太小,那麼他本身會啟用Nagle演算法(可配置是否啟用)對較小的資料包進行合併(基於此,TCP的網路延遲要UDP的高些)然後再發送(超時或者包大小足夠)。那麼這樣的話,伺服器在接收到訊息(資料流)的時候就無法區分哪些資料包是客戶端自己分開發送的,這樣產生了粘包;伺服器在接收到資料後,放到緩衝區中,如果訊息沒有被及時從快取區取走,下次在取資料的時候可能就會出現一次取出多個數據包的情況,造成粘包現象

什麼是封包與解包?

TCP/IP 網路資料以流的方式傳輸,資料流是由包組成,如何判定接收方收到的包是否是一個完整的包就要在傳送時對包進行處理,這就是封包技術,將包處理成包頭,包體。
包頭是包的開始標記,整個包的大小就是包的結束標。

如何自定義協議?

傳送時資料包是由包頭+資料 組成的:其中包頭內容分為包型別+包長度。

接收時,只需要先保證將資料包的包頭讀完整,通過收到的資料包包頭裡的資料長度和資料包型別,判斷出我們將要收到一個帶有什麼樣型別的多少長度的資料。然後迴圈接收直到接收的資料大小等於資料長度停止,此時我們完成接收一個完整資料包。

程式碼實現:

接下來是我寫的一個客戶端和伺服器的程式碼。檔案的讀取和建立寫入,是使用的linux下的系統呼叫。伺服器是單執行緒epoll。通過使用自己定義的通訊協議實現客戶端向伺服器傳送檔案。程式碼我用c++簡單的封裝了一下,重點是資料包的定義,以及傳送資料和接收資料時包的處理程式碼(protocol.h,server_recv(),send_to_serv())。

標頭檔案protocol.h:

含有資料包型別的定義,並且我把客戶端和伺服器共同需要的函式與型別定義也放進去了。

//protocol.h

#ifndef _PROTOCOL_H
#define _PROTOCOL_H

#define NET_PACKET_DATA_SIZE 5000

/// 網路資料包包頭
struct NetPacketHeader
{
    unsigned short      wDataSize;  ///< 資料包大小,包含封包頭和封包資料大小
    unsigned short      wOpcode;    ///< 操作碼
};

/// 網路資料包
struct NetPacket
{
    NetPacketHeader     Header;                         ///< 包頭
    unsigned char       Data[NET_PACKET_DATA_SIZE];     ///< 資料
};



/// 網路操作碼
enum eNetOpcode
{
    NET_TEST1  = 1,  //傳送檔案資訊
    NET_TEST2=2     //傳送檔案內容
};


struct File_message
{
    char filename[100];  //檔名
    long filesize;  //檔案大小
};

struct File_data
{
    char filename[100];  //檔名
    unsigned char buffer[1024]; //檔案內容
};

void my_err(const char *err_string,int line)  //自定義錯誤函式
{
    std::cerr<<"line:"<<line<<std::endl; //輸出錯誤發生在第幾行
    perror(err_string);       //輸出錯誤資訊提示
    exit(1);
}

#endif

#### **伺服器程式碼:**

#include<iostream>
#include<cstring>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/signal.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<errno.h>

#include"protocol.h"

using namespace std;

 
#define PORT 6666   //伺服器埠
#define LISTEN_SIZE 1023   //連線請求佇列的最大長度
#define EPOLL_SIZE  1023   //epoll監聽客戶端的最大數目
  

class TCPServer
{
    public: 

    TCPServer();
    ~TCPServer();
    
     /// 接受客戶端接入
    void acceptClient();

    /// 關閉客戶端
    void closeClient(int i);
    //處理接收到的資料
    bool dealwithpacket(int conn_fd,unsigned char *recv_data,uint16_t wOpcode,int datasize);

    bool server_recv(int conn_fd);  //接收資料函式

    void run();  //執行函式
    
    private:

    int sock_fd;  //監聽套接字
    int conn_fd;    //連線套接字
    int epollfd;  //epoll監聽描述符
    socklen_t cli_len;  //記錄連線套接字地址的大小
    struct epoll_event  event;   //epoll監聽事件
    struct epoll_event*  events;  //epoll監聽事件集合
    struct sockaddr_in cli_addr;  //客戶端地址
    struct sockaddr_in serv_addr;   //伺服器地址


};

TCPServer::TCPServer()  //建構函式
{

    //建立一個套接字
    sock_fd=socket(AF_INET,SOCK_STREAM,0);
    if(sock_fd<0)
    {
        my_err("socket",__LINE__);
    }
    //設定該套接字使之可以重新繫結埠
    int optval=1;
    if(setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,(void*)&optval,sizeof(int))<0)
    {
        my_err("setsock",__LINE__);
    }
    //初始化伺服器端地址結構
    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_port=htons(PORT);
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    if(bind(sock_fd,(struct sockaddr*)&serv_addr,sizeof(struct sockaddr_in))<0)
    {
        my_err("bind",__LINE__);
    }
    //將套接字轉化為監聽套接字
    if(listen(sock_fd,LISTEN_SIZE)<0)
    {
        my_err("listen",__LINE__);
    }


    cli_len=sizeof(struct sockaddr_in);
    events=(struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //分配記憶體空間

    //建立一個監聽描述符epoll,並將監聽套接字加入監聽列表
    epollfd=epoll_create(EPOLL_SIZE);
    if(epollfd==-1)
    {
        my_err("epollfd",__LINE__);
    }
    event.events = EPOLLIN;
    event.data.fd = sock_fd;
    if(epoll_ctl(epollfd,EPOLL_CTL_ADD,sock_fd,&event)<0)
    {
        my_err("epoll_ctl",__LINE__);
    }
    
}

TCPServer::~TCPServer()   //解構函式
{
    close(sock_fd);    //關閉監聽套接字
    cout<<"伺服器成功退出"<<endl;
}

void TCPServer::acceptClient()      //接受客戶端連線請求
{
    conn_fd=accept(sock_fd,(struct sockaddr*)&cli_addr,&cli_len);
    if(conn_fd<0)
    {
        my_err("accept",__LINE__);
    }
    event.events = EPOLLIN | EPOLLRDHUP; //監聽連線套接字的可讀和退出
    event.data.fd = conn_fd;
    if(epoll_ctl(epollfd,EPOLL_CTL_ADD,conn_fd,&event)<0) //將新連線的套接字加入監聽
    {
        my_err("epoll",__LINE__);
    }
    cout<<"a connet is connected,ip is "<<inet_ntoa(cli_addr.sin_addr)<<endl;
}


void TCPServer::closeClient(int i)     //處理客戶端退出
{
    cout<<"a connet is quit,ip is "<<inet_ntoa(cli_addr.sin_addr)<<endl;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,&event);
    close(events[i].data.fd);

}

bool TCPServer::dealwithpacket(int conn_fd,unsigned char *recv_data,uint16_t wOpcode,int datasize)  //處理接收到的資料
{
    

    int fd;
    if(wOpcode==1)  //接收檔案資訊
    { 
        File_message *file_message=(File_message*)recv_data;
        strcat(file_message->filename,".down");
        

        if((fd=open(file_message->filename,O_RDWR|O_APPEND|O_CREAT,0777))<0)
        {
            cout<<"建立檔案失敗"<<endl;
            return false;
        }
    }
    else if(wOpcode==2)  //接收檔案內容
    {
        
        File_data * file_data=(File_data*)recv_data;

        strcat(file_data->filename,".down");
        if((fd=open(file_data->filename,O_RDWR|O_APPEND))<0)
        {
            cout<<"開啟檔案失敗"<<endl;
            return false;
        }
        if(write(fd,file_data->buffer,datasize-sizeof(file_data->filename))<0)
        {
            cout<<"寫入檔案失敗"<<endl;
            return false;
        }
        close(fd);
    }
   return true;
    
}

bool TCPServer::server_recv(int conn_fd)  //接收資料函式
{
    int nrecvsize=0; //一次接收到的資料大小
    int sum_recvsize=0; //總共收到的資料大小
    int packersize;   //資料包總大小
    int datasize;     //資料總大小
    unsigned char recv_buffer[10000];  //接收資料的buffer


    memset(recv_buffer,0,sizeof(recv_buffer));  //初始化接收buffer
    

    while(sum_recvsize!=sizeof(NetPacketHeader))
    {
        nrecvsize=recv(conn_fd,recv_buffer+sum_recvsize,sizeof(NetPacketHeader)-sum_recvsize,0);
        if(nrecvsize==0)
        {
            cout<<"從客戶端接收資料失敗"<<endl;
            return false;
        }
        sum_recvsize+=nrecvsize;

    }
    


    NetPacketHeader *phead=(NetPacketHeader*)recv_buffer;
    packersize=phead->wDataSize;  //資料包大小
    datasize=packersize-sizeof(NetPacketHeader);     //資料總大小

    


    while(sum_recvsize!=packersize)
    {
        nrecvsize=recv(conn_fd,recv_buffer+sum_recvsize,packersize-sum_recvsize,0);
        if(nrecvsize==0)
        {
            cout<<"從客戶端接收資料失敗"<<endl;
            return false;
        }
        sum_recvsize+=nrecvsize;
    }

    
    dealwithpacket(conn_fd,(unsigned char*)(phead+1),phead->wOpcode,datasize);  //處理接收到的資料



}

void TCPServer::run()  //主執行函式
{
    while(1)   //迴圈監聽事件
    {
        int sum=0,i;
        sum=epoll_wait(epollfd,events,EPOLL_SIZE,-1);
        for(i=0;i<sum;i++)
        {
            if(events[i].data.fd==sock_fd)    //客戶端請求連線
            {
                acceptClient();  //處理客戶端的連線請求

            }
            else if(events[i].events&EPOLLIN)    //客戶端發來資料
            {
                
                server_recv(events[i].data.fd);  //接收資料包並做處理
                
            }
            if(events[i].events&EPOLLRDHUP) //客戶端退出
            {
                closeClient(i);    //處理客戶端退出
            }
            
        }
    }
}

int main()
{
    TCPServer server;
    server.run();

    return 0;
}

客戶端程式碼:

#include<iostream>
#include<string.h>
#include<math.h>
#include<sys/signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>

#include"protocol.h"

#define PORT 6666   //伺服器埠

using namespace std;

class TCPClient
{
    public:

    TCPClient(int argc ,char** argv);
    ~TCPClient();


    //向伺服器傳送資料
    bool send_to_serv(unsigned char *data_buffer,int datasize,uint16_t wOpcode);
    bool send_file();   //向伺服器傳送檔案
    void run(); //主執行函式


    private:

    int conn_fd; //建立連線套接字
    struct sockaddr_in serv_addr; //儲存伺服器地址

};




TCPClient::TCPClient(int argc,char **argv)  //建構函式
{
    if(argc!=3)    //檢測輸入引數個數是否正確
    {
        cout<<"Usage: [-a] [serv_address]"<<endl;
        exit(1);
    }


    //初始化伺服器地址結構
    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_port=htons(PORT);

    //從命令列伺服器地址
    for(int i=0;i<argc;i++)
    {
        if(strcmp("-a",argv[i])==0)
        {

            if(inet_aton(argv[i+1],&serv_addr.sin_addr)==0)
            {
                cout<<"invaild server ip address"<<endl;
                exit(1);
            }
            break;
        }
    }

    //檢查是否少輸入了某項引數
    if(serv_addr.sin_addr.s_addr==0)
    {
        cout<<"Usage: [-a] [serv_address]"<<endl;
        exit(1);
    }

    //建立一個TCP套接字
    conn_fd=socket(AF_INET,SOCK_STREAM,0);


    if(conn_fd<0)
    {
        my_err("connect",__LINE__);
    }

    //向伺服器傳送連線請求
    if(connect(conn_fd,(struct sockaddr*)&serv_addr,sizeof(struct sockaddr))<0)
    {
        my_err("connect",__LINE__);
    }
    
}

TCPClient::~TCPClient()  //解構函式
{
    close(conn_fd);

}

bool TCPClient::send_to_serv(unsigned char *data_buffer,int datasize,uint16_t wOpcode) //向伺服器傳送資料
{
    NetPacket send_packet;   //資料包
    send_packet.Header.wDataSize=datasize+sizeof(NetPacketHeader);  //資料包大小
    send_packet.Header.wOpcode=wOpcode;

    memcpy(send_packet.Data,data_buffer,datasize);  //資料拷貝


    if(send(conn_fd,&send_packet,send_packet.Header.wDataSize,0))
       return true;
    else
       return false;

}


bool TCPClient::send_file()   //向伺服器傳送檔案
{
    unsigned char send_buffer[1024]; //傳送資料buffer
    string filename;  //檔案路徑名
    int fd; //檔案描述符
    struct stat file_buffer;  //檔案屬性buffer
    File_message file_message;   //檔案資訊
    File_data file_data;    //儲存檔案的內容和大小
    int nsize=0;     //一次讀取的資料大小
    int sum_size=0;     //總共讀取的資料大小



    memset(send_buffer,0,sizeof(send_buffer));

    cout<<"請輸入要傳送的檔案路徑及檔名"<<endl;
    getline(cin,filename);

    if(fd=open(filename.c_str(),O_RDONLY)<0)
    {
        my_err("open file error",__LINE__);
    }
    if(stat(filename.c_str(),&file_buffer)<0)
    {
        my_err("stat file error",__LINE__);
    }

    strcpy(file_message.filename,filename.c_str());
    file_message.filesize=file_buffer.st_size;



    if(send_to_serv((unsigned char*)&file_message,sizeof(file_data),NET_TEST1)<0)
    {
        cout<<"向伺服器傳送檔案資訊失敗"<<endl;
    }


    close(fd);
    if((fd=open(filename.c_str(),O_RDONLY))<0)
       {
           my_err("開啟檔案失敗",__LINE__);
       }

    while(nsize=read(fd,send_buffer,sizeof(send_buffer)))
    {


        memset(&file_data,0,sizeof(file_data));
        strcpy(file_data.filename,filename.c_str());

        memcpy(file_data.buffer,send_buffer,nsize);

        send_to_serv((unsigned char*)&file_data,nsize+sizeof(file_data.filename),NET_TEST2);
        sum_size+=nsize;
    }



    if(sum_size==file_buffer.st_size)
    {
        cout<<"傳送檔案成功"<<endl;
        close(fd);
        return true;
    }
    else
    {
        cout<<"傳送檔案出錯"<<endl;
        close(fd);
        return false;
    }

}

void TCPClient::run()
{
    send_file();  //向伺服器傳送檔案
    
}

int main(int argc ,char **argv)
{
   
    TCPClient client(argc,argv);
    client.run();
    sleep(10);

}

相關推薦

TCP定義通訊協議

我們為什麼要自定義TCP應用層傳輸協議? 因為在TCP流傳輸的過程中,可能會出現分包與黏包的現象。我們為了解決這些問題,需要我們自定義通訊協議進行封包與解包。 什麼是分包與黏包? 分包:指接受方沒有接受到一個完整的包,只接受了部分。 黏包:指傳送方傳送的若干包資料到接收

[Golang] 從零開始寫Socket Server(2): 定義通訊協議

        在上一章我們做出來一個最基礎的demo後,已經可以初步實現Server和Client之間的資訊交流了~ 這一章我會介紹一下怎麼在Server和Client之間實現一個簡單的通訊協議,從而增強整個資訊交流過程的穩定性。  

一個簡單的定義通訊協議 socket

                這是轉自javaeye的一篇文章,作者是vtrtbb。按照網路通訊的傳統,我們都會自定義協議,這有很多好處,大家可以自己體會(嘿嘿)。一直不知道socket通訊時候自定義資料包是什麼樣子的,偶然做了個小例子。先來說說資料包的定義,我這裡是包頭+內容 組成的:其中包頭內容分為包型

Arduino定義通訊協議解析

/* 自定義的庫函式: 協議解析器 V1.0 解析的資料格式: 協議首部-指令長度-控制指令-校驗和 "控制指令"格式: 裝置型別-裝置號-埠號 */ #include<stdlib.h> #include<string.h> //#pragma warning(disab

IOT 定義通訊協議

裝置型別(1位元組) 裝置廠商(1位元組) 協議版本號(8位元組) 命令碼(1位元組) 幀序號(4位元組) 傳送接收標識(1位元組)) 資料包長度(2位元組,高位元組在前,低位元組在後)

Netty4.0學習筆記系列之五:定義通訊協議

實現的原理是通過Encoder把java物件轉換成ByteBuf流進行傳輸,通過Decoder把ByteBuf轉換成java物件進行處理,處理邏輯如下圖所示: 傳輸的Java bean為Person: package com.guowl.testobjcoder

一個簡單的定義通訊協議(socket)

這是轉自javaeye的一篇文章,作者是vtrtbb。 按照網路通訊的傳統,我們都會自定義協議,這有很多好處,大家可以自己體會(嘿嘿)。 一直不知道socket通訊時候自定義資料包是什麼樣子的,偶然做了個小例子。 先來說說資料包的定義,我這裡是包頭+內容 組

一個定義通訊協議的erlang socket多人線上聊天程式

利用自定義的erlang通訊協議編寫的入門socket程式,可實現簡單的多人線上聊天程式。 服務端的程式碼: -module(server_example). -export([start/0,initialize_ets/0,info_

Java乾貨之Socket定義傳輸協議,可用於一般即時通訊

原型 客戶端 Client package me.mxzf; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; imp

mac下定義協議配置

xxx element https src internet -c pos tro six 之前查了很多資料,最近也在挖掘研究這方面的漏洞. windows的很簡單,在註冊表配置就好了,但是mac os 是unix的,沒有註冊表這麽一說。 但是發現騰訊等配置了自定義等協

http協議是用於從全球資訊網伺服器傳輸超文字到本地瀏覽器的傳送協議。所有www都遵從這個協議。http是一個基於TCP/IP的通訊協議來傳遞html 檔案 查詢結果 圖片檔案等

HTTP 工作原理 HTTP協議工作於客戶端-服務端架構上。瀏覽器作為HTTP客戶端通過URL向HTTP服務端即WEB伺服器傳送所有請求。 Web伺服器有:Apache伺服器,IIS伺服器(Internet Information Services)等。 Web伺服器根據接收到的請求後

TCP,IP通訊協議

一個 http 請求,在整個網路中的請求過程 當應用程式用T C P傳送資料時,資料被送入協議棧中, 然後逐個通過每一層直到被當作一串位元流送入網路。其 中每一層對收到的資料都要增加一些首部資訊 當目的主機收到一個乙太網資料幀時,資料就開始從協議 棧中由底向上升,同時去掉各層協議加上的報文

可以傳中文引數的定義http協議請求方式

package com.system.util.juxinli; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.http.HttpEnt

用socket定義簡單協議實現檔案上傳與接受

一個上傳的資料包,主要包含檔案頭和檔案內容倆部分,主要按下面的格式,傳送: "File-Name:xxxxxx.zip;File-Type:exe;File-Length:1029292\r\n" ------檔案內容--------- 1、服務端的檔案接受服務 MySoc

當 webview 遇到定義協議

當我們使用webview開啟自定義的schem的url時,如果不做處理會提示找不到網頁,如下圖:藍色顯示的url 是 baidumap://這個webview識別不了,所以我們要做一些處理,其次我們要理解這個自定義的協議給我們是用來幹嘛的。 好了話不多說  處理如下:

IE瀏覽器 定義地址協議的實現

關鍵字:IE外掛,shell介面程式設計,自定義IE協議,VC2003 ATL 實現COM 瀏覽QQ空間的時候發現,只要在IE地址中輸入象一下這種形式的地址,tencent://Message/?Uin=251464630&w

定義瀏覽器協議,實現web程式呼叫本地程式

參考了一下qq的方式。 tencent://Message/?Uin=000000&websiteName=qzone.qq.com&Menu=yes 在登錄檔裡面新增下面,就能實現 Windows Registry Editor Version 5.00

Chapter 6 定義資料協議【第十三講】 通過大端序列方法將4個位元組int轉成 byte陣列

第一種方法: 通過大端序列方法將4個位元組int轉成 byte陣列大端序列方法:int2bytes:將int 從高到低位分別儲存到 byte[0] ~ byte[3]    byte[] bytes = new byte[4];     for (int i = 0; i &

通過定義URL協議在Web網頁中啟動本地應用程式

    在做web應用的時候,我們經常會遇到在web中呼叫本地應用程式的問題,例如在web中點選一個按鈕,然後開啟自己寫的或者別人的應用程式。上網比較留意的同學應該會發現,想qq客服,淘寶的阿里旺旺客服都會有這樣的效果。     下面我主要介紹一種現在主流的處理方法,這種方

定義HID協議、應用說明

基本框架:     HID裝置支援USB標準描述符中的五個:裝置描述符、配置描述符、介面描述符、端點描述符、字串描述符。除此之外,HID裝置還有三種特殊的描述符:HID描述符、報告描述符、物理描述符。一個USB裝置只能支援一個HID描述符,但可以支援多個報告描述符,而物理