Window网络编程之简单TCP建立

步骤分解

  • 服务端建立

    步骤说明涉及函数
    建立服务端SOCKETsocket
    绑定服务IP和端口bind
    监听网络端口listen
    等待建立连接accept
    发送数据send
    关闭服务端SOCKETclosesocket
  • 客户端建立

    步骤说明涉及函数
    建立客户端SOCKETsocket
    连接服务器connect
    接收数据recv
    关闭服务端SOCKETclosesocket

搭建SOCKET开发环境

开发环境说明

window下进行SOCKET编程依赖两个库如下所示:

  • 较早的DLL是wsock32.dll,对应的头文件为winsock1.h
  • 最新的DLL是ws2_32.dll,对应的头文件为winsock2.h

基本所有的window操作系统都已经支持了ws2_32.dll,因此可以直接使用这个库无需考虑第一个比较早的库。除了导入头文件之外还需要导入ws2_32.lib链接库,可使用显示导入方式

#pragma comment (lib, "ws2_32.lib")

如果在vs开发环境下也可以在项目属性中添加依赖库,如果使用Clion则在CMakeLists中add_executable语句前增加一条语句

link_libraries(ws2_32)

启动SOCKET说明

SOCKET标准的启动过程如下:

#include <iostream>
#include <WinSock2.h>

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);
    // ......
    WSACleanup();
    return 0;
}

使用MAKEWORD(2, 2)来明确SOCKET的版本号,WinSock规范的最新版本号为2.2版本,wsock32.dll仅支持1.01.1wsock32.dll已经能够很好的支持TCP/IP通信程序的开发,ws2_32.dll主要增加了对其他协议的支持,建议使用最新的2.2版本。

如果当前工程中还需要用到window.h头文件,需要在引入任何头文件之前增加WIN32_LEAN_AND_MEAN宏定义,减少对较早SOCKET版本的依赖,如果能保证window.hWinSock2.h之后引入也可不添加次宏定义。

创建TCP服务器

创建SOCKET

原型

int socket(int af, int type, int protocol);

参数说明:

字段说明参数填写
af地址族,使用IP的类型AF_INET = IPv4,AF_INET6 = IPv6
type数据传输方式SOCK_STREAM = 面向连接,SOCK_DGRAM = 面向连接
protocol传输协议IPPROTO_TCP,IPPTOTO_UDP

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    SOCKET server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    WSACleanup();
    return 0;
}

绑定端口

原型

int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);

参数说明:

字段说明参数填写
sockSOCKET对象使用socket创建的对象后返回的对象
addrSOCKET地址结构体外部创建好并填写,IP、端口等信息
addrlenSOCKET地址结构体长度-

结构体定义:

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    // ......

     绑定地址和端口 
    SOCKADDR_IN sock_add_in;
    sock_add_in.sin_family = AF_INET;
    sock_add_in.sin_port = htons(9090);
    sock_add_in.sin_addr.S_un.S_addr = INADDR_ANY;
    int bind_result = bind(server_socket, (SOCKADDR*)&sock_add_in, sizeof(SOCKADDR_IN));
    if (bind_result == SOCKET_ERROR) {
        std::cout << "bind error." << std::endl;
    }

    WSACleanup();
    return 0;
}

其中INADDR_ANY表示当前服务器可以接收任意客户端请求,也可填写具体IP地址,地址填写的时候使用inet_addr函数进行转换。端口号的填写也是一样的需要使用htons函数进行转换。

在进行绑定时需要将SOCKADDR_IN强转换成SOCKADDR结构体,这两个结构体的长度是相同的都是16字节,可以简单的认为SOCKADDR是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而SOCKADDR_IN是专门用来保存IPv4地址的结构体。另外还有SOCKADDR_IN6_LH是专门用来保存IPv6地址的结构体。

监听连接

原型

int listen(SOCKET sock, int backlog);

参数说明:

字段说明参数填写
sockSOCKET对象使用socket创建的对象后返回的对象
backlog请求队列的最大长度-

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    // ......

     监听连接 
    int listen_result = listen(server_socket, 5);
    if (listen_result == SOCKET_ERROR) {
        std::cout << "listen error." << std::endl;
    }

    WSACleanup();
    return 0;
}

listen使SOCKET进入监听状态,其中backlog为请求队列的最大长度动监听是指当没有客户端请求时,SOCKET处于睡眠状态,只有当接收到客户端请求时,SOCKET才会被唤醒来响应请求。

当SOCKET正在处理客户端请求时,如果有新的请求进来SOCKET是没法处理,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(即,能存放多少个客户端请求)可以通过backlog参数指定,可以根据需求来定,并发量小的话可以是10或者20,或者将backlog的值设置为SOMAXCONN由系统来决定请求队列长度,这个值一般比较大可能是几百或者更多。

当请求队列满时,就不再接收新的请求,客户端会收到WSAECONNREFUSED错误。

连接客户端

原型

SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);

参数说明:

字段说明参数填写
sockSOCKET对象使用socket创建的对象后返回的对象
addr客户端SOCKET地址结构体空结构体在接收连接请求的时候填写填充客户端信息
addrlen客户端SOCKET地址结构体客户端SOCKET地址结构体的长度

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    // ......

     处理连接 
    SOCKADDR_IN client_add_in = { 0 };
    int client_lent = sizeof(SOCKADDR_IN);
    SOCKET client_socket = INVALID_SOCKET;
    client_socket = accept(server_socket, (SOCKADDR*)&client_add_in, &client_lent);
    if (client_socket == INVALID_SOCKET) {
        std::cout << "client connect error." << std::endl;
    }

    WSACleanup();
    return 0;
}

listen只是让SOCKET进入监听状态并没有真正接收客户端请求,listen后面的代码会继续执行,accept才是处理请求的函数,accept会阻塞当前线程执行。

accept返回一个新的SOCKET来和客户端通信,其中SOCKADDR_IN结构会被填充,描述了客户端的IP地址端口号

向客户端发送数据

原型

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

参数说明:

字段说明参数填写
sockSOCKET对象使用accept后返回的SOCKET对象
buf发送数据包缓存地址发送的实际数据地址
len发送的数据的字节数-
flags为发送数据时的选项一般填0

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    // .....
    
     发送数据 
    char message[] = "Build TCP Connect.";
    send(client_socket, message, strnlen_s(message, 100), 0);

    WSACleanup();
    return 0;
}

关闭服务器

原型

closesocket(SOCKET s);

参数说明:

字段说明参数填写
sSOCKET对象使用socket创建的对象后返回的对象

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    // ......

     关闭服务器 
    closesocket(server_socket);

    WSACleanup();
    return 0;
}

创建TCP客户端

创建SOCKET

客户端创建SOCKET的步骤与服务器创建SOCKET步骤一致。

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    SOCKET client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    WSACleanup();
    return 0;
}

连接服务器

原型

int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);

参数和bind一样,不同的是客户端连接的时候需要指定服务器的具体IP。

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    // ......

     连接服务器 
    SOCKADDR_IN sock_add_in;
    sock_add_in.sin_family = AF_INET;
    sock_add_in.sin_port = htons(9090);
    sock_add_in.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

    int connect_result = connect(client_socket, (sockaddr*)&sock_add_in, sizeof(SOCKADDR_IN));
    if (connect_result == SOCKET_ERROR) {
        std::cout << "connect error." << std::endl;
    }

    WSACleanup();
    return 0;
}

接收数据

原型

int recv(SOCKET sock, char *buf, int len, int flags);

参数说明:

字段说明参数填写
sockSOCKET对象使用socket创建的SOCKET对象
buf接收数据包缓存地址接收到数据后存放的内存地址
len数据缓存区的大小-
flags为发送数据时的选项一般填0

使用

int main(int argc, char * argv[]) {
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);

    // ......

     接收数据 
    int recv_len = recv(client_socket, message, 256, 0);
    if (recv_len > 0) {
        std::cout << "recv message : %s" << message << std::endl;
    }

    WSACleanup();
    return 0;
}

recv返回实际接收到的数据包的大小,可以根据返回值判断数据是否正确,最后关闭socket即可。

运行结果

TCP连接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值