0
点赞
收藏
分享

微信扫一扫

C++网络编程

祈澈菇凉 2022-03-25 阅读 74
java后端

网络编程

1. TCP与UDP的比较

TCP是面向连接的,交互双方的进程各自建立一个流式套接字,服务器需要等待客户端向其提出连接申请。一旦接受客户端申请就立刻返回一个新的套接字描述符。通过该描述符调用数据传输函数与客户端进行数据的收发。

UDP是面向无连接的,双方建立的是数据报套接字,服务器和客户端在进行传描数据之前不需要进行连接的申请和建立,可以随时向对方发消息。

TCP

优点:可靠、稳定

缺点:速度慢,效率低、占用系统资源高、易被攻击。

适合场景:网络通讯质量要求高(可靠、稳定)

UDP

优点:速度快,比TCP稍安全

缺点:不可靠、不稳定

适用场合:网络通讯质量要求不高,速度快。

2. Socket粘包问题

什么时候需要考虑粘包问题

1:如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要要双方都发送close连接(参考tcp关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符;

2:如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包;

3:如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:

1)“hellogive me sth abour yourself”

2)“Don’tgive me sth abour yourself”

那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hello give me sth abour yourselfDon’t give me sth abouryourself" 这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。

粘包出现原因:

在流传输中出现,UDP不会出现粘包,因为它有消息保护边界。

1 发送端需要等缓冲区满才发送出去,造成粘包

2 接收方不及时接收缓冲区的包,造成多个包接收

解决办法:

为了避免粘包现象,可采取以下几种措施:

一是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;

二是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;

三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

以上提到的三种措施,都有其不足之处。

第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。

第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。

第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。

更为简洁的说法:

定包长

包尾加

包头加包体长度

网上说法:

个人比较喜欢的一种做法是给一帧数据加帧头帧尾,然后接收方不断接受并缓存收到的数据,根据帧头帧尾分离出一帧完整的数据,再分离各字段得到数据。

如果某个包出错了,怎么不断恢复?

发送消息时,每个消息长度在编程的时候就指定了。如果接收到的数据包有问题,我们可以通过消息长度来不断回复原来的数据包。

3. TCP例子

服务端:

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, “ws2_32.lib”)

int _tmain(int argc, _TCHAR* argv[])

{

WSADATA wsaData;

int port = 5099;

char buf[] = "服务器: 欢迎登录… ";

// 加载套接字

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

{

printf("加载套接字失败:%d… ", WSAGetLastError());

return 1;

}

// socket()

SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

// 初始化IP和端口信息

SOCKADDR_IN addrSrv;

addrSrv.sin_family = AF_INET;

addrSrv.sin_port = htons(port); // 1024以上的端口号

addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

// bind()

if (bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)

{

printf("套接字绑定失败:%d… ", WSAGetLastError());

return 1;

}

// listen()

if (listen(sockSrv, 10) == SOCKET_ERROR){

printf("套接字监听失败:%d… ", WSAGetLastError());

return 1;

}

// 客户端信息

SOCKADDR_IN addrClient;

int len = sizeof(SOCKADDR);

// 开始监听

printf("服务端启动成功…开始监听… ");

while (1)

{

// 等待客户请求到来

SOCKET sockConn = accept(sockSrv, (SOCKADDR *)&addrClient, &len);

if (sockConn == SOCKET_ERROR){

printf("建立连接失败:%d… ", WSAGetLastError());

break;

}

printf("与客户端建立连接…IP:[%s] ", inet_ntoa(addrClient.sin_addr));

// 发送数据

if (send(sockConn, buf, sizeof(buf), 0) == SOCKET_ERROR){

printf("发送数据失败… ");

break;

}

char recvBuf[100];

memset(recvBuf, 0, sizeof(recvBuf));

// 接收数据

recv(sockConn, recvBuf, sizeof(recvBuf), 0);

printf("收到数据:%s ", recvBuf);

closesocket(sockConn);

}

// 关闭套接字

closesocket(sockSrv);

WSACleanup();

system(“pause”);

return 0;

}

客户端:

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, “ws2_32.lib”)

int _tmain(int argc, _TCHAR* argv[])

{

WSADATA wsaData;

int port = 5099;

char buff[1024];

memset(buff, 0, sizeof(buff));

// 加载套接字

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

{

printf("加载套接字失败:%d… ", WSAGetLastError());

return 1;

}

// 初始化IP和端口信息

SOCKADDR_IN addrSrv;

addrSrv.sin_family = AF_INET;

addrSrv.sin_port = htons(port);

addrSrv.sin_addr.S_un.S_addr = inet_addr(“127.0.0.1”);

// socket()

SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

if (SOCKET_ERROR == sockClient){

printf("创建套接字失败:%d… ", WSAGetLastError());

return 1;

}

// 向服务器发出连接请求

if (connect(sockClient, (struct sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET)

{

printf("连接服务器失败:%d… ", WSAGetLastError());

return 1;

}

else

{

// 接收数据

recv(sockClient, buff, sizeof(buff), 0);

printf("收到数据:%s ", buff);

// 发送数据

char buf[] = “客户端:请求登录…”;

send(sockClient, buf, sizeof(buf), 0);

}

// 关闭套接字

closesocket(sockClient);

WSACleanup();

return 0;

}

旧函数解决方式:

4. UDP例子

服务端(接收方):

// UDPReceiverTest.cpp : 定义控制台应用程序的入口点。

//

#include “stdafx.h”

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, “ws2_32.lib”)

int _tmain(int argc, _TCHAR* argv[])

{

WSADATA wsaData;

int port = 5099;

// 加载套接字

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

{

printf("加载套接字失败:%d… ", WSAGetLastError());

return 1;

}

// 初始化IP和端口信息

SOCKADDR_IN addrSrv;

addrSrv.sin_family = AF_INET;

addrSrv.sin_port = htons(port);

addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

// socket()

SOCKET sockClient = socket(AF_INET,SOCK_DGRAM, 0);

if (SOCKET_ERROR == sockClient){

printf("创建套接字失败:%d… ", WSAGetLastError());

return 1;

}

// bind()

if (bind(sockClient, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)

{

printf("套接字绑定失败:%d… ", WSAGetLastError());

return 1;

}

SOCKADDR_IN addrClnt;

int nLen = sizeof(SOCKADDR);

// 消息

char szMsg[1024];

memset(szMsg, 0, sizeof(szMsg));

// 等待客户请求到来

printf("服务端启动成功…等待客户发送数据… ");

while (1)

{

// 接收数据

if (SOCKET_ERROR != recvfrom(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrClnt, &nLen))

{

printf("发送方:%s ", szMsg);

char szSrvMsg[] = “收到…”;

// 发送数据

sendto(sockClient, szSrvMsg, sizeof(szSrvMsg), 0, (SOCKADDR*)&addrClnt, nLen);

}

}

// 上面为无线循环,以下代码不会执行

// 关闭套接字

closesocket(sockClient);

WSACleanup();

return 0;

}

客户端:

// UDPSenderTest.cpp : 定义控制台应用程序的入口点。

//

#include “stdafx.h”

#include <stdio.h>

#include <WinSock2.h>

#pragma comment(lib, “ws2_32.lib”)

int _tmain(int argc, _TCHAR* argv[])

{

WSADATA wsaData;

int port = 5099;

// 加载套接字

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)

{

printf("加载套接字失败:%d… ", WSAGetLastError());

return 1;

}

// socket()

SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);

if (SOCKET_ERROR == sockClient){

printf("创建套接字失败:%d… ", WSAGetLastError());

return 1;

}

// 初始化IP和端口信息

SOCKADDR_IN addrSrv;

addrSrv.sin_family = AF_INET;

addrSrv.sin_port = htons(port);

addrSrv.sin_addr.S_un.S_addr = inet_addr(“127.0.0.1”);

int nLen = sizeof(SOCKADDR);

// 发送数据

char szMsg[1024];

memset(szMsg, 0, sizeof(szMsg));

sendto(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, nLen);

// 发送数据

while (1)

{

// 初始化数据

char szMsg[1024];

memset(szMsg, 0, sizeof(szMsg));

printf(“请输入要发送的数据(输入q退出):”);

scanf("%s", &szMsg);

// 退出循环

if (!strcmp(szMsg, “q”) || !strcmp(szMsg, “Q”))

{

break;

}

// 发送数据

sendto(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, nLen);

// 清空缓存

memset(szMsg, 0, sizeof(szMsg));

// 接收数据

if (SOCKET_ERROR != recvfrom(sockClient, szMsg, sizeof(szMsg), 0, (SOCKADDR*)&addrSrv, &nLen))

{

printf("接收方:%s ", szMsg);

}

}

// 关闭套接字

closesocket(sockClient);

WSACleanup();

return 0;

}

5. TCP和 UDP 注意点

易忽略,出错的地方:socket()

TCP: SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

UDP: SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);

TCP不存在数据边界:

收到数据不意味着马上调用read()函数,只要不超过数组容量,则有可能数据填充满缓冲后通过一次read()函数调用读取全部,也有可能分成多次read()函数调用进行读取。如果传输出错就会提供重传服务。(套接字内部有一个由字节数组构成的缓冲)

6. 结构体、图片传输方法

首先通讯双方需要统一结构体,示例:

struct Massage

{

int nID;

char strMsg[64];

};

发送方:

// 结构体消息

Massage stMsg;

memset(stMsg.strMsg, 0, sizeof(stMsg.strMsg));

stMsg.nID = 1001;

strcpy(stMsg.strMsg, “Struct string”);

// …

sendto(sockClient, (char*)&stMsg, sizeof(stMsg) + 1, 0, (SOCKADDR*)&addrClnt, nLen);

接收方:

// 结构体

Massage stMsg;

memset(stMsg.strMsg, 0, sizeof(stMsg.strMsg));

memcpy(&stMsg, szMsg, sizeof(stMsg) + 1);

printf("接收方:%d %s ", stMsg.nID, stMsg.strMsg);

特别注意: sizeof(stMsg) + 1 两者必须保持一致。

拓展:发送文件

// 图片

struct Photo

{

int nSize;

char buf[256];

};

Photo stPhoto;

memset(stPhoto.buf, 0, sizeof(stPhoto.buf));

// 发送文件

printf("正在发送文件… ");

while (fp1)

{

// 读取文件内容到buf中,每次读256字节,返回值表示实际读取的字节数

int nCount = fread(stPhoto.buf, 1, sizeof(stPhoto.buf), fp1);

stPhoto.nSize = nCount;

//printf("read %d byte ", nCount);

// 如果读取的字节数不大于0,说明读取出错或文件已经读取完毕

if (nCount <= 0)

{

sprintf(stPhoto.buf, "finish ");

sendto(sockClient, (char*)&stPhoto, sizeof(stPhoto), 0, (SOCKADDR*)&addrSrv, nLen);

printf("文件发送完成… ");

break;

}

sendto(sockClient, (char*)&stPhoto, sizeof(stPhoto), 0, (SOCKADDR*)&addrSrv, nLen);

}

接收文件:

printf("正在接收文件… ");

while (1)

{

// 接收图片

if (SOCKET_ERROR != recvfrom(sockClient, szFileInfo, sizeof(szFileInfo), 0, (SOCKADDR*)&addrClnt, &nLen))

{

memcpy(&stPhoto, szFileInfo, sizeof(stPhoto));

if (0 == strncmp(stPhoto.buf, “finish”, 6))

{

printf("文件接收完成… ");

break;

}

int n = fwrite(stPhoto.buf, 1, stPhoto.nSize, fp2);

//printf("write %d byte ", n);

}

}

7. 常见错误

包含<windows.h>和winsock.h后重定义问题:

[解决方案]

由以上代码可以看出如果在没有定义WIN32_LEAN_AND_MEAN宏的大前

提下windows.h有可能包含winsock.h 头文件,因此我们得出一个很简单

的解决方法就是在包含<windows.h>之前定义WIN32_LEAN_AND_MEAN宏,如

下所示:

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

举报

相关推荐

0 条评论