Socket通信之TCP通信小程序

Resurgam

发布日期: 2018-11-13 12:46:41 浏览量: 824
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

背景

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

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

函数介绍

socket 函数

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

函数声明

  1. SOCKET WSAAPI socket(
  2. _In_ int af,
  3. _In_ int type,
  4. _In_ int protocol
  5. );

参数

  • 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 函数

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

函数声明

  1. int bind(
  2. _In_ SOCKET s,
  3. _In_ const struct sockaddr *name,
  4. _In_ int namelen
  5. );

参数

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

返回值

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

htons 函数

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

函数声明

  1. u_short WSAAPI htons(
  2. _In_ u_short hostshort
  3. );

参数

  • hostshort [in]

    主机字节顺序为16位。

返回值

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

inet_addr 函数

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

函数声明

  1. unsigned long inet_addr(
  2. _In_ const char *cp
  3. );

参数

  • cp [in]

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

返回值

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

listen函数

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

函数声明

  1. int listen(
  2. _In_ SOCKET s,
  3. _In_ int backlog
  4. );

参数

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

返回值

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

accept 函数

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

函数声明

  1. SOCKET accept(
  2. _In_ SOCKET s,
  3. _Out_ struct sockaddr *addr,
  4. _Inout_ int *addrlen
  5. );

参数

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

返回值

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

send 函数

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

函数声明

  1. int send(
  2. _In_ SOCKET s,
  3. _In_ const char *buf,
  4. _In_ int len,
  5. _In_ int flags
  6. );

参数

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

返回值

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

recv 函数

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

函数声明

  1. int recv(
  2. _In_ SOCKET s,
  3. _Out_ char *buf,
  4. _In_ int len,
  5. _In_ int flags
  6. );

参数

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

返回值

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

实现原理

如下图所示:

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

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

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

编码实现

导入库文件

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

服务器端

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

  1. // 绑定端口并监听
  2. BOOL SocketBindAndListen(char *lpszIp, int iPort)
  3. {
  4. // 初始化 Winsock 库
  5. WSADATA wsaData = {0};
  6. ::WSAStartup(MAKEWORD(2, 2), &wsaData);
  7. // 创建流式套接字
  8. g_ServerSocket = ::socket(AF_INET, SOCK_STREAM, 0);
  9. if (INVALID_SOCKET == g_ServerSocket)
  10. {
  11. return FALSE;
  12. }
  13. // 设置服务端地址和端口信息
  14. sockaddr_in addr;
  15. addr.sin_family = AF_INET;
  16. addr.sin_port = ::htons(iPort);
  17. addr.sin_addr.S_un.S_addr = ::inet_addr(lpszIp);
  18. // 绑定IP和端口
  19. if (0 != ::bind(g_ServerSocket, (sockaddr *)(&addr), sizeof(addr)))
  20. {
  21. return FALSE;
  22. }
  23. // 设置监听
  24. if (0 != ::listen(g_ServerSocket, 1))
  25. {
  26. return FALSE;
  27. }
  28. // 创建接收数据多线程
  29. ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL);
  30. return TRUE;
  31. }

服务器端数据发送。

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

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

  1. // 接受连接请求 并 接收数据
  2. void AcceptRecvMsg()
  3. {
  4. sockaddr_in addr = { 0 };
  5. // 注意:该变量既是输入也是输出
  6. int iLen = sizeof(addr);
  7. // 接受来自客户端的连接请求
  8. g_ClientSocket = ::accept(g_ServerSocket, (sockaddr *)(&addr), &iLen);
  9. printf("accept a connection from client!\n");
  10. char szBuf[MAX_PATH] = { 0 };
  11. while (TRUE)
  12. {
  13. // 接收数据
  14. int iRet = ::recv(g_ClientSocket, szBuf, MAX_PATH, 0);
  15. if (0 >= iRet)
  16. {
  17. continue;
  18. }
  19. printf("[recv]%s\n", szBuf);
  20. }
  21. }

客户端

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

  1. // 连接到服务器
  2. BOOL Connection(char *lpszServerIp, int iServerPort)
  3. {
  4. // 初始化 Winsock 库
  5. WSADATA wsaData = { 0 };
  6. ::WSAStartup(MAKEWORD(2, 2), &wsaData);
  7. // 创建流式套接字
  8. g_ClientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
  9. if (INVALID_SOCKET == g_ClientSocket)
  10. {
  11. return FALSE;
  12. }
  13. // 设置服务端地址和端口信息
  14. sockaddr_in addr = { 0 };
  15. addr.sin_family = AF_INET;
  16. addr.sin_port = ::htons(iServerPort);
  17. addr.sin_addr.S_un.S_addr = ::inet_addr(lpszServerIp);
  18. // 连接到服务器
  19. if (0 != ::connect(g_ClientSocket, (sockaddr *)(&addr), sizeof(addr)))
  20. {
  21. return FALSE;
  22. }
  23. // 创建接收数据多线程
  24. ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL);
  25. return TRUE;
  26. }

客户端发送数据。

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

客户端接收数据。

  1. // 接收数据
  2. void RecvMsg()
  3. {
  4. char szBuf[MAX_PATH] = { 0 };
  5. while (TRUE)
  6. {
  7. // 接收数据
  8. int iRet = ::recv(g_ClientSocket, szBuf, MAX_PATH, 0);
  9. if (0 >= iRet)
  10. {
  11. continue;
  12. }
  13. printf("[recv]%s\n", szBuf);
  14. }
  15. }

程序测试

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

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

总结

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

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

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

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

参考

参考自《Windows黑客编程技术详解》一书

上传的附件 cloud_download TCP_Test.7z ( 176.62kb, 21次下载 )

发送私信

每个人最终和自己越长越像

15
文章数
15
评论数
最近文章
eject