一个简单的TCP自定义通信协议

我们为什么要自定义TCP应用层传输协议?

因为在TCP流传输的过程中,可能会出现分包与黏包的现象。我们为了解决这些问题,需要我们自定义通信协议进行封包与解包。

什么是分包与黏包?

分包:指接受方没有接受到一个完整的包,只接受了部分。
黏包:指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

产生分包与黏包现象的原因是什么?

产生分包原因:

可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。

产生黏包的原因:

由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象

什么是封包与解包?

TCP/IP 网络数据以流的方式传输,数据流是由包组成,如何判定接收方收到的包是否是一个完整的包就要在发送时对包进行处理,这就是封包技术,将包处理成包头,包体。
包头是包的开始标记,整个包的大小就是包的结束标。

如何自定义协议?

发送时数据包是由包头+数据 组成的:其中包头内容分为包类型+包长度。

接收时,只需要先保证将数据包的包头读完整,通过收到的数据包包头里的数据长度和数据包类型,判断出我们将要收到一个带有什么样类型的多少长度的数据。然后循环接收直到接收的数据大小等于数据长度停止,此时我们完成接收一个完整数据包。

代码实现:

<h4 id="接下来是我写的一个客户端和服务器的代码文件的读取和创建写入是使用的linux下的 系统调用服务器是单线程epoll通过使用自己定义的通信协议实现客户端向服务器发送文件代码我用c简单的封装了一下重点是数据包的定义以及发送数据和接收数据时包的处理代码protocolhserverrecvsendtoserv">接下来是我写的一个客户端和服务器的代码。文件的读取和创建写入,是使用的linux下的系统调用。服务器是单线程epoll。通过使用自己定义的通信协议实现客户端向服务器发送文件。代码我用c++简单的封装了一下,重点是数据包的定义,以及发送数据和接收数据时包的处理代码(protocol.h,server_recv(),send_to_serv())。
头文件protocol.h:

含有数据包类型的定义,并且我把客户端和服务器共同需要的函数与类型定义也放进去了。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
//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; endif= "" pre= "" ><h4 id= "服务器代码" ><strong>服务器代码:</strong></h4><pre class = "brush:java;" >#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
#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; a= "" bool= "" char = "" conn_fd= "accept(sock_fd,(struct" connet= "" event.data.fd= "conn_fd;" event.events= "EPOLLIN" file_message= "(File_message*)recv_data;" file_message-= "" int = "" ip= "" is= "" uint16_t= "" unsigned= "" void = "" wopcode= "=1)" >filename, ".down" );
 
 
         if ((fd=open(file_message->filename,O_RDWR|O_APPEND|O_CREAT, 0777 ))< 0 )
         {
             cout<< "创建文件失败" <<endl; else = "" file_data= "" file_data-= "" return = "" wopcode= "=2)" >filename, ".down" );
         if ((fd=open(file_data->filename,O_RDWR|O_APPEND))< 0 )
         {
             cout<< "打开文件失败" <<endl; file_data-= "" return = "" >buffer,datasize-sizeof(file_data->filename))< 0 )
         {
             cout<< "写入文件失败" <<endl; bool= "" buffer= "" char = "" int = "" netpacketheader= "" nrecvsize= "=0)" packersize= "phead-" phead= "(NetPacketHeader*)recv_buffer;" return = "" sum_recvsize= "0;" unsigned= "" >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; phead-= "" return = "" unsigned= "" >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++) .data.fd= "=sock_fd)" else = "" int = "" pre= "" return = "" tcpserver= "" ><h4 id= "客户端代码" ><strong>客户端代码:</strong></h4><pre class = "brush:java;" >#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
#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++)></errno.h></arpa></netinet></fcntl.h></sys></sys></sys></unistd.h></sys></math.h></string.h></iostream></pre>
</sum;i++)></endl;></endl;></endl;></endl;></endl;></errno.h></sys></arpa></netinet></sys></sys></sys></unistd.h></fcntl.h></sys></cstring></iostream></pre>
</line<<std::endl;>
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值