通信编程:Winsock 编写 TCP 套接字

套接字编写流程#

以 TCP 套接字为例,由于 TCP 是面向连接的协议,所以基于 TCP 的套接字也需要有多个步骤。

套接字的创建#

在进行网络通信之前,都需要使用 socket() 函数创建一个套接字对象。

Copy Highlighter-hljs
SOCKET WSAAPI socket( _In_ int af, _In_ int type, _In_ int protocol );
参数 说明
af socket 使用的地址格式
type 指定套接字的类型
protocol 指定使用的协议类型

其中 WinSock 中只支持 AF_INET 作为地址格式,一般 type 确定后 protocol 也会随之确定。socket 的 type 可以是以下几种类型:

type 类型 说明
SOCK STREAM 流套接字 使用TCP提供有连接的可靠的传输
SOCK DGRAM 数据报套接字 使用UDP提供无连接的不可靠的传输
SOCK RAW 原始套接字 不使用某种特定的协议去封装它,而是由程序自行处理数据报以及协议首部

绑定 socket 和地址#

创建了 socket 对象后,需要为该对象绑定 IP 地址和端口号,需要使用到 bind() 函数

Copy Highlighter-hljs
bind( _In_ SOCKET s, _In_reads_bytes_(namelen) const struct sockaddr FAR * name, _In_ int namelen );
参数 说明
s 套接字句柄
name 要关联的本地地址
namelen 地址长度

一般来说 s 就是刚刚创建的 socket 对象,name 可一个 sockaddr_in 结构,namelen 则直接对一个 sockaddr_in 结构用 sizeof() 运算即可。

进入监听状态#

绑定地址后,socket 就可以进入监听状态,这个时候就可以接收传来的链接信息了。为了进入监听状态,需要使用 listen() 函数

Copy Highlighter-hljs
listen( _In_ SOCKET s, _In_ int backlog );
参数 说明
s 套接字句柄
backlog 监听队列的长度

接收连接请求#

客户端想要与服务器建立一条 TCP 连接,需要使用 connect() 函数

Copy Highlighter-hljs
int WSAAPI connect( _In_ SOCKET s, _In_reads_bytes_(namelen) const struct sockaddr FAR * name, _In_ int namelen );

服务器使用 accept() 函数将在监听队列中,取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄。新创建的套接字是处理实际连接的套接字,它与 s 有相同的属性。

Copy Highlighter-hljs
accept( _In_ SOCKET s, _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr, _Inout_opt_ int FAR * addrlen );

connect() 函数和 accept() 函数的参数相同:

参数 说明
s 套接字句柄
name 要连接的设备的地址信息
namelen 地址长度

name 中的地址用来寻址远程的 socket,一般来说监听状态下是一个循环等待的过程。此时程序默认工作在阻塞模式下,如果没有未处理的连接存在,accept() 函数会一直等待下去,直到有新的连接发生才返回。

收发数据#

对于流套接字来说,一般使用 send() 函数来发送缓冲区内的数据,返回发送数据的实际字节数。

Copy Highlighter-hljs
send( _In_ SOCKET s, _In_reads_bytes_(len) const char FAR * buf, _In_ int len, _In_ int flags );
参数 说明
s 套接字句柄
buf 要发送的数据
len 要发送的数据的长度
flags 调用方式,通常为 0

可以使用 recv() 函数从对方接收数据,并将其存储到指定的缓冲区。

Copy Highlighter-hljs
recv( _In_ SOCKET s, _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, _In_ int len, _In_ int flags );
参数 说明
s 套接字句柄
buf 接收的数据要存储的变量
len 能接收的数据的长度
flags 调用方式,通常为 0

在阻塞模式下,send 将会阻塞线程的执行直到所有的数据发送完毕(或者发生错误),而 recv 函数将返回尽可能多的当前可用信息,直到达到缓冲区指定的大小。

关闭套接字#

当不使用 socket 创建的套接字时,应该调用 closesocket() 函数将它关闭。

Copy Highlighter-hljs
int WSAAPI closesocket( _In_ SOCKET s );
参数 说明
s 套接字句柄

TCP 套接字样例#

功能设计#

模拟实现 TCP 协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。也就是在一条 TCP 连接中,客户端和服务器相互发送一条数据即可。

程序工作流程#

由于前面的流程是对于单个客户端或服务器的编码流程,这里给出一组客户端和服务器工作的流程。

编码实现#

注意无论是客户端还是服务器,都需要包含头文件 initsock.h 来载入 Winsock。

initsock.h#

Copy Highlighter-hljs
#include <winsock2.h> #pragma comment(lib, "WS2_32") // 链接到 WS2_32.lib class CInitSock { public: /*CInitSock 的构造器*/ CInitSock(BYTE minorVer = 2, BYTE majorVer = 2) { // 初始化WS2_32.dll WSADATA wsaData; WORD sockVersion = MAKEWORD(minorVer, majorVer); if (::WSAStartup(sockVersion, &wsaData) != 0) { exit(0); } } /*CInitSock 的析构器*/ ~CInitSock() { ::WSACleanup(); } };

服务器#

Copy Highlighter-hljs
#include "initsock.h" #include <iostream> using namespace std; CInitSock initSock; // 初始化Winsock库 int main() { // 创建套接字 SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sListen == INVALID_SOCKET) { cout << "Failed socket()" << endl; return 0; } // 填充sockaddr_in结构 sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(4567); sin.sin_addr.S_un.S_addr = INADDR_ANY; // 绑定这个套接字到一个本地地址 if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) { cout << "Failed bind()" << endl; return 0; } // 进入监听模式 if (::listen(sListen, 2) == SOCKET_ERROR) { cout << "Failed listen()" << endl; return 0; } // 循环接受客户的连接请求 sockaddr_in remoteAddr; int nAddrLen = sizeof(remoteAddr); SOCKET sClient; char szText[] = "你好!"; while (TRUE) { cout << "服务端已启动,正在监听!\n" << endl; // 接受一个新连接 sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen); if (sClient == INVALID_SOCKET) { cout << "Failed accept()" << endl; continue; } cout << "与主机 " << inet_ntoa(remoteAddr.sin_addr) << "建立连接:" << endl; // 接收数据 char buff[256]; int nRecv = ::recv(sClient, buff, 256, 0); if (nRecv > 0) { buff[nRecv] = '\0'; cout << "接收到数据:" << buff << endl; } // 向客户端发送数据 ::send(sClient, szText, strlen(szText), 0); // 关闭同客户端的连接 ::closesocket(sClient); } // 关闭监听套接字 ::closesocket(sListen); return 0; }

客户端#

Copy Highlighter-hljs
#include "initsock.h" #include <iostream> using namespace std; CInitSock initSock; // 初始化Winsock库 int main() { // 创建套接字 SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (s == INVALID_SOCKET) { cout << " Failed socket()" << endl; return 0; } // 也可以在这里调用bind函数绑定一个本地地址,否则系统将会自动安排 // 填写远程地址信息 sockaddr_in servAddr; servAddr.sin_family = AF_INET; servAddr.sin_port = htons(4567); // 填写服务器程序(TCPServer程序)所在机器的IP地址 char serverAddr[] = "127.0.0.1"; servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr); //与服务器建立连接 if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1) { cout << " Failed connect()" << endl; return 0; } cout << "与服务器 " << serverAddr << "建立连接" << endl; //向服务器发送数据 char szText[] = "你好,服务器!"; int slen = send(s, szText, 100, 0); if (slen > 0) { cout << "向服务器发送数据:" << szText << endl; } // 接收数据 char buff[256]; int nRecv = ::recv(s, buff, 256, 0); if (nRecv > 0) { buff[nRecv] = '\0'; cout << "接收到数据:" << buff << endl; } // 关闭套接字 ::closesocket(s); return 0; }

运行效果#

参考资料#

《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社

posted @   乌漆WhiteMoon  阅读(805)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2020-10-10 网络技术:子网划分
点击右上角即可分享
微信分享提示
CONTENTS