通信编程: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 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社

posted @ 2021-10-10 00:48  乌漆WhiteMoon  阅读(765)  评论(0编辑  收藏  举报