C++网络编程

网络编程

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软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;

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

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

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

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

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

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

更为简洁的说法:

定包长

包尾加\r\n

包头加包体长度

网上说法:

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

 

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

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

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[] = "服务器: 欢迎登录......\n";

    // 加载套接字

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

    {

         printf("加载套接字失败:%d......\n", 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......\n", WSAGetLastError());

         return 1;

    }

    // listen()

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

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

         return 1;

    }

    // 客户端信息

    SOCKADDR_IN addrClient;

    int len = sizeof(SOCKADDR);

    // 开始监听

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

    while (1)

    {

         // 等待客户请求到来  

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

         if (sockConn == SOCKET_ERROR){

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

             break;

         }

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

         // 发送数据

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

             printf("发送数据失败......\n");

             break;

         }

         char recvBuf[100];

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

         // 接收数据

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

         printf("收到数据:%s\n", 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......\n", 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......\n", WSAGetLastError());

         return 1;

    }

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

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

    {

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

         return 1;

    }

    else

    {

         // 接收数据

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

         printf("收到数据:%s\n", 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......\n", 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......\n", WSAGetLastError());

         return 1;

    }

    // bind()

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

    {

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

         return 1;

    }

    SOCKADDR_IN addrClnt;

    int nLen = sizeof(SOCKADDR);

    // 消息

    char szMsg[1024];

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

    // 等待客户请求到来

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

    while (1)

    {

         // 接收数据

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

         {

             printf("发送方:%s\n", 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......\n", WSAGetLastError());

         return 1;

    }

    // socket()

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

    if (SOCKET_ERROR == sockClient){

         printf("创建套接字失败:%d......\n", 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\n", 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\t%s\n", stMsg.nID, stMsg.strMsg);

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

拓展:发送文件

    // 图片

    struct Photo

    {

         int nSize;

         char buf[256];

    };

    Photo stPhoto;

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

    // 发送文件

    printf("正在发送文件......\n");

    while (fp1)

    {

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

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

         stPhoto.nSize = nCount;

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

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

         if (nCount <= 0)

         {

             sprintf(stPhoto.buf, "finish\n");

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

             printf("文件发送完成......\n");

             break;

         }

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

    }

接收文件:

    printf("正在接收文件......\n");

    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("文件接收完成......\n");

                  break;

             }

            

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

             //printf("write %d byte\n", 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>

最后推荐17个C++网络编程实战练手项目,你会几个?
1.C++通过HTTP下载文件
2.C++字符串通过URL编码
3.使用Poco C++库快速实现一个简单HTTP服务器
4.C++实现一个简单UDP服务端及客户端
5.C++实现一个简单TCP服务端及客户端
6.MFC实现一个简单TCP服务端及客户端
7.C++封装一个实用TCP客户端操作类
8.C++通过FTP下载文件简单示例
9.C++多线程FTP客户端示例
10.C++封装一个实用FTP客户端操作类
11.MFC实现点对点的大文件传输
12.C++多线程实现并发TCP客户端测试程序
13.C++多线程实现TCP服务器端同时和多个客户端通信
14.手把手教你了解并解决TCP粘包问题
15.Libevent网络库实现简单TCP服务端及客户端
16.libcurl实现上传文件支持中文路径
17.C++使用Websocket++实现WebSocket客户端通信

  • 46
    点赞
  • 359
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苦逼的IT男

你的鼓励是我最大的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值