通信编程:Winsock 编写 TCP 套接字
套接字编写流程
以 TCP 套接字为例,由于 TCP 是面向连接的协议,所以基于 TCP 的套接字也需要有多个步骤。
套接字的创建
在进行网络通信之前,都需要使用 socket() 函数创建一个套接字对象。
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() 函数。
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() 函数。
listen(
_In_ SOCKET s,
_In_ int backlog
);
参数 | 说明 |
---|---|
s | 套接字句柄 |
backlog | 监听队列的长度 |
接收连接请求
客户端想要与服务器建立一条 TCP 连接,需要使用 connect() 函数。
int
WSAAPI
connect(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const struct sockaddr FAR * name,
_In_ int namelen
);
服务器使用 accept() 函数将在监听队列中,取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄。新创建的套接字是处理实际连接的套接字,它与 s 有相同的属性。
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() 函数来发送缓冲区内的数据,返回发送数据的实际字节数。
send(
_In_ SOCKET s,
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags
);
参数 | 说明 |
---|---|
s | 套接字句柄 |
buf | 要发送的数据 |
len | 要发送的数据的长度 |
flags | 调用方式,通常为 0 |
可以使用 recv() 函数从对方接收数据,并将其存储到指定的缓冲区。
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() 函数将它关闭。
int
WSAAPI
closesocket(
_In_ SOCKET s
);
参数 | 说明 |
---|---|
s | 套接字句柄 |
TCP 套接字样例
功能设计
模拟实现 TCP 协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。也就是在一条 TCP 连接中,客户端和服务器相互发送一条数据即可。
程序工作流程
由于前面的流程是对于单个客户端或服务器的编码流程,这里给出一组客户端和服务器工作的流程。
编码实现
注意无论是客户端还是服务器,都需要包含头文件 initsock.h 来载入 Winsock。
initsock.h
#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();
}
};
服务器
#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;
}
客户端
#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 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社