C/C++ Socket通信之TCP通信小程序

背景

之前自己做过关于Socket通信的视频教程,主要是教大家怎么使用Windows提供的Socket函数接口去实现网络通信的。当时讲了两个小程序的实现,一个是TCP通信,另一个是UDP通信。

如今,我把视频教程讲解的内容,重新整理成文档的形式,并对程序简化,使用使用控制台重新开发,方便初学者的理解。本文先讲解使用Socket实现TCP通信的小程序,先把程序实现过程和原理整理成文档,分享给大家。

函数介绍

socket 函数

根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数。

函数声明

SOCKET WSAAPI socket(
      _In_ int af,
      _In_ int type,
      _In_ int protocol
);

参数

  • af [in]
    地址族规范。 地址系列的可能值在Winsock2.h头文件中定义。当前支持的值为AF_INET或AF_INET6,它们是IPv4和IPv6的Internet地址族格式。
  • type[in]
    指定Socket类型,SOCK_STREAM类型指定产生流式套接字,SOCK_DGRAM类型指定产生数据报式套接字,而SOCK_RAW类型指定产生原始套接字(只有管理员权限用户才能创建原始套接字)。
  • protocol[in]
    与特定的地址家族相关的协议IPPROTO_TCP、IPPROTO_UDP和IPPROTO_IP,如果指定为0,那么系统就会根据地址格式和套接字类别,自动选择一个合适的协议。

返回值

  • 如果没有发生错误,套接字返回引用新套接字的描述符。 否则,返回值为INVALID_SOCKET,并且可以通过调用WSAGetLastError来检索特定的错误代码。

bind 函数

将本地地址与套接字相关联。

函数声明

int bind(
     _In_ SOCKET                s,
     _In_ const struct sockaddr *name,
     _In_ int                   namelen
);

参数

  • s [in]
    标识未绑定套接字的描述符。
  • 名称[in]
    指向本地地址的sockaddr结构的指针,以分配给绑定的套接字。
  • namelen [in]
    name参数指向的值的长度(以字节为单位)。

返回值

  • 如果没有发生错误,则bind返回零。 否则,它返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

htons 函数

将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。

函数声明

u_short WSAAPI htons(
      _In_ u_short hostshort
);

参数

  • hostshort [in]

    主机字节顺序为16位。

返回值

  • 返回TCP / IP网络字节顺序。

inet_addr 函数

将一个点分十进制的IP转换成一个长整数型数。

函数声明

unsigned long inet_addr(
       _In_ const char *cp
);

参数

  • cp [in]

    点分十进制的IP字符串,以NULL结尾。

返回值

  • 如果没有发生错误,则inet_addr函数将返回一个无符号长整型值,其中包含给定的Internet地址的适当的二进制表示形式。

listen函数

将一个套接字置于正在监听传入连接的状态。

函数声明

int listen(
      _In_ SOCKET s,
      _In_ int    backlog
);

参数

  • s [in]
    标识绑定的未连接套接字的描述符。
  • backlog[in]
    待连接队列的最大长度。 如果设置为SOMAXCONN,负责套接字的底层服务提供商将积压设置为最大合理值。 如果设置为SOMAXCONN_HINT(N)(其中N是数字),则积压值将为N,调整为范围(200, 65535)。

返回值

  • 如果没有发生错误,listen将返回零。否则,返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

accept 函数

允许在套接字上进行连接尝试。

函数声明

SOCKET accept(
      _In_    SOCKET          s,
      _Out_   struct sockaddr *addr,
      _Inout_ int             *addrlen
);

参数

  • s [in]
    一个描述符,用于标识使用listen功能处于侦听状态的套接字。 连接实际上是由accept返回的套接字。
  • addr [out]
    一个可选的指向缓冲区的指针,它接收通信层已知的连接实体的地址。 addr参数的确切格式由创建sockaddr结构的套接字时建立的地址族确定。
  • addrlen [in,out]
    指向一个整数的可选指针,其中包含addr参数指向的结构长度。

返回值

  • 如果没有发生错误,则accept返回一个SOCKET类型的值,该值是新套接字的描述符。 此返回值是实际连接所在的套接字的句柄。
  • 否则,返回值为INVALID_SOCKET,并且可以通过调用WSAGetLastError来检索特定的错误代码。

send 函数

在建立连接的套接字上发送数据。

函数声明

int send(
      _In_       SOCKET s,
      _In_ const char   *buf,
      _In_       int    len,
      _In_       int    flags
);

参数

  • s [in]
    标识连接的套接字的描述符。
  • buf [in]
    指向包含要发送的数据的缓冲区的指针。
  • len [in]
    由buf参数指向的缓冲区中数据的长度(以字节为单位)。
  • 标志[in]
    一组指定呼叫方式的标志。

返回值

  • 如果没有发生错误,发送返回发送的总字节数,可以小于len参数中要发送的数量。 否则,返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

recv 函数

从连接的套接字或绑定的无连接套接字接收数据。

函数声明

int recv(
      _In_  SOCKET s,
      _Out_ char   *buf,
      _In_  int    len,
      _In_  int    flags
);

参数

  • s [in]
    标识连接的套接字的描述符。
  • buf [out]
    指向缓冲区的指针,用于接收传入的数据。
  • len [in]
    由buf参数指向的缓冲区的长度(以字节为单位)。
  • 标志[in]
    一组影响此功能的行为的标志。

返回值

  • 如果没有发生错误,则recv返回接收的字节数,buf参数指向的缓冲区将包含接收到的数据。 如果连接已正常关闭,返回值为0。

实现原理

如下图所示:

img

无论对服务器端来说还是客户端来说,都首先要初始化Winsock服务环境。

服务器端初始化Winsock环境后,便调用 socket 函数创建流式套接字;然后对sockaddr_in结构体进行设置,设置服务器绑定的IP地址和端口等信息并调用 bind 函数绑定;绑定成功后,便可以调用 listen 函数设置连接数量,并进行监听。直到有来自客户端的连接请求,服务器便调用 accept 函数接受连接请求,建立连接。这时,便可以使用 recv 函数和 send 函数与客户端进行数据的收发。通信结束后,变关闭套接字,释放资源。

客户端初始化环境后,便调用 socket 函数创建流式套接字;然后对sockaddr_in结构体进行设置,设置服务器的IP地址和端口等信息并调用 connect 函数向服务器发送连接请求,并等待服务器的响应。服务器接受连接请求后,便成功与服务器建立连接,这时,便可以使用 recv 函数和 send 函数与客户端进行数据的收发。通信结束后,变关闭套接字,释放资源。

编码实现

导入库文件

#include <Winsock2.h>
#pragma comment(lib, "Ws2_32.lib")

服务器端

初始化Winsock库环境,创建流式套接字,绑定服务器IP地址和端口,并进行监听。

// 绑定端口并监听
BOOL SocketBindAndListen(char *lpszIp, int iPort)
{
    // 初始化 Winsock 库
    WSADATA wsaData = {0};
    ::WSAStartup(MAKEWORD(2, 2), &wsaData);
    // 创建流式套接字
    g_ServerSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (INVALID_SOCKET == g_ServerSocket)
    {
        return FALSE;
    }
    // 设置服务端地址和端口信息
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = ::htons(iPort);
    addr.sin_addr.S_un.S_addr = ::inet_addr(lpszIp);
    // 绑定IP和端口
    if (0 != ::bind(g_ServerSocket, (sockaddr *)(&addr), sizeof(addr)))
    {
        return FALSE;
    }
    // 设置监听
    if (0 != ::listen(g_ServerSocket, 1))
    {
        return FALSE;
    }
    // 创建接收数据多线程
    ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL);
    return TRUE;
}

服务器端数据发送。

// 发送数据
void SendMsg(char *pszSend)
{
    // 发送数据
    ::send(g_ClientSocket, pszSend, (1 + ::lstrlen(pszSend)), 0);
    printf("[send]%s\n", pszSend);
}

服务器端接收连接请求并接收数据。

// 接受连接请求 并 接收数据
void AcceptRecvMsg()
{
    sockaddr_in addr = { 0 };
    // 注意:该变量既是输入也是输出
    int iLen = sizeof(addr);   
    // 接受来自客户端的连接请求
    g_ClientSocket = ::accept(g_ServerSocket, (sockaddr *)(&addr), &iLen);
    printf("accept a connection from client!\n");
    char szBuf[MAX_PATH] = { 0 };
    while (TRUE)
    {
        // 接收数据
        int iRet = ::recv(g_ClientSocket, szBuf, MAX_PATH, 0);
        if (0 >= iRet)
        {
            continue;
        }
        printf("[recv]%s\n", szBuf);
    }
}

客户端

初始化Winsock库环境,创建流式套接字,并连接服务器。

// 连接到服务器
BOOL Connection(char *lpszServerIp, int iServerPort)
{
    // 初始化 Winsock 库
    WSADATA wsaData = { 0 };
    ::WSAStartup(MAKEWORD(2, 2), &wsaData);
    // 创建流式套接字
    g_ClientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (INVALID_SOCKET == g_ClientSocket)
    {
        return FALSE;
    }
    // 设置服务端地址和端口信息
    sockaddr_in addr = { 0 };
    addr.sin_family = AF_INET;
    addr.sin_port = ::htons(iServerPort);
    addr.sin_addr.S_un.S_addr = ::inet_addr(lpszServerIp);
    // 连接到服务器
    if (0 != ::connect(g_ClientSocket, (sockaddr *)(&addr), sizeof(addr)))
    {
        return FALSE;
    }
    // 创建接收数据多线程
    ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL);
    return TRUE;
}

客户端发送数据。

// 发送数据
void SendMsg(char *pszSend)
{
    // 发送数据
    ::send(g_ClientSocket, pszSend, (1 + ::lstrlen(pszSend)), 0);
    printf("[send]%s\n", pszSend);
}

客户端接收数据。

// 接收数据
void RecvMsg()
{
    char szBuf[MAX_PATH] = { 0 };
    while (TRUE)
    {
        // 接收数据
        int iRet = ::recv(g_ClientSocket, szBuf, MAX_PATH, 0);
        if (0 >= iRet)
        {
            continue;
        }
        printf("[recv]%s\n", szBuf);
    }
}

程序测试

我们进行本机测试,服务器地址端口为:127.0.0.1:12345。

先运行服务器,进行绑定并监听,然后再运行客户端进行连接,连接成功后,就可以相互进行数据通信。

img

总结

有 3 个地方如果稍不注意的话,便很容易出错:

一是在使用Socket函数之前,一定要对Winsock服务进行初始化,初始化是由WSAStartup函数实现的。如果不进行初始化操作,而直接使用Socket函数,会报错。

二是对于服务端中的接受来自客户端连接请求的accept函数,第三个参数一定要格外注意,它是既是输入参数也是输出参数,也就是说,一定要给它一个初值,初值大小就是sockaddr_in结构体的大小。

三是测试的时候,如果服务端和客户端通信一直不成功,可以试着使用CMD命令的ping指令ping下两台主机是否能ping通,若不能,则检查是否在同一网段内或者防火墙是否关闭;若ping通,则检查自己的代码是否有误,可以单步进行调试。

//网络素材仅限收藏 方便学习

posted @ 2021-06-06 15:42  New_HackerHK  阅读(322)  评论(0)    收藏  举报