TCP/IP 网络数据包和非数据包
最编程
2024-04-19 19:24:13
...
这是一个网上的代码;下面列出资料并简略分析代码;
TCP/IP 网络数据封包和解包
.
TCP/IP 网络数据以流的方式传输,数据流是由包组成,如何判定接收方收到的包是否是一个完整的包就要在发送时对包进行处理,这就是封包技术,将包处理成包头,包体
包头是包的开始标记,整个包的大小就是包的结束标记。接收方只要按同样的方式解包即可,下面是一个网络服务端和客户端程序代码。
客户端和服务端共享的文件:(数据包的定义)
01.#pragma once
02.
03.
04.#define NET_PACKET_DATA_SIZE 1024
05.#define NET_PACKET_SIZE (sizeof(NetPacketHeader) + NET_PACKET_DATA_SIZE) * 10
06.
07.
08./// 网络数据包包头
09.struct NetPacketHeader
10.{
11. unsigned short wDataSize; ///< 数据包大小,包含封包头和封包数据大小
12. unsigned short wOpcode; ///< 操作码
13.};
14.
15./// 网络数据包
16.struct NetPacket
17.{
18. NetPacketHeader Header; ///< 包头
19. unsigned char Data[NET_PACKET_DATA_SIZE]; ///< 数据
20.};
21.
22.
23.
24.//////////////////////////////////////////////////////////////////////////
25.
26.
27./// 网络操作码
28.enum eNetOpcode
29.{
30. NET_TEST1 = 1,
31.};
32.
33./// 测试1的网络数据包定义
34.struct NetPacket_Test1
35.{
36. int nIndex;
37. char name[20];
38. char sex[20];
39. int age;
40. char arrMessage[512];
41.};
服务端:
[cpp] view plaincopyprint?
01.#pragma once
02.
03.class TCPServer
04.{
05.public:
06. TCPServer();
07. virtual ~TCPServer();
08.
09.public:
10. void run();
11.
12. /// 接受客户端接入
13. void acceptClient();
14.
15. /// 关闭客户端
16. void closeClient();
17.
18. /// 发送数据
19. bool SendData(unsigned short nOpcode, const char* pDataBuffer, const unsigned int& nDataSize);
20.
21.private:
22. SOCKET mServerSocket; ///< 服务器套接字句柄
23. sockaddr_in mServerAddr; ///< 服务器地址
24.
25. SOCKET mAcceptSocket; ///< 接受的客户端套接字句柄
26. sockaddr_in mAcceptAddr; ///< 接收的客户端地址
27.
28. char m_cbSendBuf[NET_PACKET_SIZE];
29.};
[cpp] view plaincopyprint?
01.#include "stdafx.h"
02.
03.
04.TCPServer::TCPServer()
05.: mServerSocket(INVALID_SOCKET)
06.{
07. // 创建套接字
08. mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
09. if (mServerSocket == INVALID_SOCKET)
10. {
11. std::cout << "创建套接字失败!" << std::endl;
12. return;
13. }
14.
15. // 填充服务器的IP和端口号
16. mServerAddr.sin_family = AF_INET;
17. mServerAddr.sin_addr.s_addr = INADDR_ANY;
18. mServerAddr.sin_port = htons((u_short)SERVER_PORT);
19.
20. // 绑定IP和端口
21. if ( ::bind(mServerSocket, (sockaddr*)&mServerAddr, sizeof(mServerAddr)) == SOCKET_ERROR)
22. {
23. std::cout << "绑定IP和端口失败!" << std::endl;
24. return;
25. }
26.
27. // 监听客户端请求,最大同时连接数设置为10.
28. if ( ::listen(mServerSocket, SOMAXCONN) == SOCKET_ERROR)
29. {
30. std::cout << "监听端口失败!" << std::endl;
31. return;
32. }
33.
34. std::cout << "启动TCP服务器成功!" << std::endl;
35.}
36.
37.TCPServer::~TCPServer()
38.{
39. ::closesocket(mServerSocket);
40. std::cout << "关闭TCP服务器成功!" << std::endl;
41.}
42.
43.void TCPServer::run()
44.{
45. // 接收客户端的连接
46. acceptClient();
47.
48. int nCount = 0;
49. for (;;)
50. {
51. if (mAcceptSocket == INVALID_SOCKET)
52. {
53. std::cout << "客户端主动断开了连接!" << std::endl;
54. break;
55. }
56.
57. // 发送数据包
58. NetPacket_Test1 msg;//消息类型
59. msg.nIndex = nCount;
60. msg.age=23;
61. strncpy(msg.arrMessage, "北京市朝阳区", sizeof(msg.arrMessage) );
62. strncpy(msg.name, "天策", sizeof(msg.name) );
63. strncpy(msg.sex, "男", sizeof(msg.sex) );
64.
65. bool bRet = SendData(NET_TEST1, (const char*)&msg, sizeof(msg));//强制类型转换为字符串类型
66. if (bRet)
67. {
68. std::cout << "发送数据成功!" << std::endl;
69. }
70. else
71. {
72. std::cout << "发送数据失败!" << std::endl;
73. break;
74. }
75.
76. ++nCount;
77. }
78.}
79.
80.void TCPServer::closeClient()
81.{
82. // 判断套接字是否有效
83. if (mAcceptSocket == INVALID_SOCKET) return;
84.
85. // 关闭客户端套接字
86. ::closesocket(mAcceptSocket);
87. std::cout << "客户端套接字已关闭!" << std::endl;
88.}
89.
90.void TCPServer::acceptClient()
91.{
92. // 以阻塞方式,等待接收客户端连接
93. int nAcceptAddrLen = sizeof(mAcceptAddr);
94. mAcceptSocket = ::accept(mServerSocket, (struct sockaddr*)&mAcceptAddr, &nAcceptAddrLen);
95. std::cout << "接受客户端IP:" << inet_ntoa(mAcceptAddr.sin_addr) << std::endl;
96.}
97.
98.bool TCPServer::SendData( unsigned short nOpcode, const char* pDataBuffer, const unsigned int& nDataSize )
99.{
100. NetPacketHeader* pHead = (NetPacketHeader*) m_cbSendBuf;
101. pHead->wOpcode = nOpcode;//操作码
102.
103. // 数据封包
104. if ( (nDataSize > 0) && (pDataBuffer != 0) )
105. {
106. CopyMemory(pHead+1, pDataBuffer, nDataSize);
107. }
108.
109. // 发送消息
110. const unsigned short nSendSize = nDataSize + sizeof(NetPacketHeader);//包的大小事发送数据的大小加上包头大小
111. pHead->wDataSize = nSendSize;//包大小
112. int ret = ::send(mAcceptSocket, m_cbSendBuf, nSendSize, 0);
113. return (ret > 0) ? true : false;
114.}
[cpp] view plaincopyprint?
01.// testTCPServer.cpp : 定义控制台应用程序的入口点。
02.//
03.
04.#include "stdafx.h"
05.
06.
07.
08.int _tmain(int argc, _TCHAR* argv[])
09.{
10. TCPServer server;
11. server.run();
12.
13. system("pause");
14. return 0;
15.}
客户端:
[cpp] view plaincopyprint?
01.<span style="font-size: 14px;">#pragma once
02.
03.class TCPClient
04.{
05.public:
06. TCPClient();
07. virtual ~TCPClient();
08.
09.public:
10. /// 主循环
11. void run();
12.
13. /// 处理网络消息
14. bool OnNetMessage(const unsigned short& nOpcode,
15. const char* pDataBuffer, unsigned short nDataSize);
16.
17. bool OnNetPacket(NetPacket_Test1* pMsg);
18.
19.private:
20. SOCKET mServerSocket; ///< 服务器套接字句柄
21. sockaddr_in mServerAddr; ///< 服务器地址
22.
23. char m_cbRecvBuf[NET_PACKET_SIZE];
24. char m_cbDataBuf[NET_PACKET_SIZE];
25. int m_nRecvSize;
26.};
27.</span>
[cpp] view plaincopyprint?
01.#include "stdafx.h"
02.
03.
04.
05.TCPClient::TCPClient()
06.{
07. memset( m_cbRecvBuf, 0, sizeof(m_cbRecvBuf) );
08. m_nRecvSize = 0;
09.
10. // 创建套接字
11. mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
12. if (mServerSocket == INVALID_SOCKET)
13. {
14. std::cout << "创建套接字失败!" << std::endl;
15. return;
16. }
17.
18. // 填充服务器的IP和端口号
19. mServerAddr.sin_family = AF_INET;
20. mServerAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
21. mServerAddr.sin_port = htons((u_short)SERVER_PORT);
22.
23. // 连接到服务器
24. if ( ::connect(mServerSocket, (struct sockaddr*)&mServerAddr, sizeof(mServerAddr)))
25. {
26. ::closesocket(mServerSocket);
27. std::cout << "连接服务器失败!" << std::endl;
28. return;
29. }
30.}
31.
32.TCPClient::~TCPClient()
33.{
34. ::closesocket(mServerSocket);
35.}
36.
37.void TCPClient::run()
38.{
39. int nCount = 0;
40. for (;;)
41. {
42. // 接收数据
43. int nRecvSize = ::recv(mServerSocket,
44. m_cbRecvBuf+m_nRecvSize,
45. sizeof(m_cbRecvBuf)-m_nRecvSize, 0);
46. if (nRecvSize <= 0)
47. {
48. std::cout << "服务器主动断开连接!" << std::endl;
49. break;
50. }
51.
52. // 保存已经接收数据的大小
53. m_nRecvSize += nRecvSize;
54.
55. // 接收到的数据够不够一个包头的长度
56. while (m_nRecvSize >= sizeof(NetPacketHeader))//已经收到一个完整的包,如果没用收到一个完整的包,此处循环不执行,继续下一轮循环
57. {
58. // 收够5个包,主动与服务器断开
59. if (nCount >= 5)
60. {
61. ::closesocket(mServerSocket);
62. break;
63. }
64.
65. // 读取包头
66. NetPacketHeader* pHead = (NetPacketHeader*) (m_cbRecvBuf);
67. const unsigned short nPacketSize = pHead->wDataSize;
68.
69. // 判断是否已接收到足够一个完整包的数据
70. if (m_nRecvSize < nPacketSize)
71. {
72. // 还不够拼凑出一个完整包
73. break;
74. }
75.
76. // 拷贝到数据缓存
77. CopyMemory(m_cbDataBuf, m_cbRecvBuf, nPacketSize);
78.
79. // 从接收缓存移除
80. MoveMemory(m_cbRecvBuf, m_cbRecvBuf+nPacketSize, m_nRecvSize);
81. m_nRecvSize -= nPacketSize;
82.
83. // 解密数据,以下省略一万字
84. // ...
85.
86. // 分派数据包,让应用层进行逻辑处理
87. pHead = (NetPacketHeader*) (m_cbDataBuf);
88. const unsigned short nDataSize = nPacketSize - (unsigned short)sizeof(NetPacketHeader);
89. OnNetMessage(pHead->wOpcode, m_cbDataBuf+sizeof(NetPacketHeader), nDataSize);
90.
91. ++nCount;
92. }
93. }
94.
95. std::cout << "已经和服务器断开连接!" << std::endl;
96.}
97.
98.bool TCPClient::OnNetMessage( const unsigned short& nOpcode,
99. const char* pDataBuffer, unsigned short nDataSize )
100.{
101. switch (nOpcode)
102. {
103. case NET_TEST1:
104. {
105. NetPacket_Test1* pMsg = (NetPacket_Test1*) pDataBuffer;
106. return OnNetPacket(pMsg);
107. }
108. break;
109.
110. default:
111. {
112. std::cout << "收取到未知网络数据包:" << nOpcode << std::endl;
113. return false;
114. }
115. break;
116. }
117.}
118.
119.bool TCPClient::OnNetPacket( NetPacket_Test1* pMsg )
120.{
121. std::cout << "索引:" << pMsg->nIndex << " 字符串:" << pMsg->arrMessage <<"name:"<<pMsg->name<<"sex:"<<pMsg->sex<<"age:"<<pMsg->age<< std::endl;
122. return true;
123.}
[cpp] view plaincopyprint?
01.#include "stdafx.h"
02.
03.
04.int _tmain(int argc, _TCHAR* argv[])
05.{
06. TCPClient client;
07. client.run();
08.
09. system("pause");
10. return 0;
11.}
下面分析一下其代码;
首先这是一个VC++程序;因为,_tmain是VC++下的类似于C的main的东东;
首先定义包头结构体,包结构体,操作码枚举;
首先运行客户端;在客户端的构造函数中,分配接收缓冲区,创建套接字,连接服务器;
run()函数运行,接收并保存包,调用OnNetMessage()进行处理;
OnNetMessage()中,如果nOpcode是NET_TEST1,调用OnNetPacket();
OnNetPacket()中输出包的内容;
服务端的构造函数,创建套接字,绑定IP和端口,监听客户端请求;
run()中,接收客户端连接,发送数据包;
服务端也是一个单独的控制台应用程序;单独启动;
推荐阅读
-
网络安全 - 数据包嗅探和伪造/欺骗实验
-
子网划分、可变长度子网掩码和 TCP/IP 故障排除__子网划分、掩码、网络概述
-
(x) 网络层--在 IP 层转发数据包的过程
-
计算机网络-4-4-转发数据包、构建子网和划分超级网
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
计算机网络 - TCP 真实世界数据包捕获分析
-
Golang Capture:实施网络数据包捕获和分析
-
开源网络数据包捕获和分析学习框架--Packetbeat 章节
-
[网络安全] Wireshark 过滤数据包并分析 TCP 三次握手 (顶部)
-
Wireshark 数据包分析实战 (I) 数据包分析和网络基础入门