• Winsock2使用记录


    本文地址:https://www.cnblogs.com/oberon-zjt0806/p/14814144.html
    WinSock 2 MSDN文档:https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-start-page-2
    一个很好的范例(如果我的代码有问题,可供参考这个):https://github.com/gaohaoning/cpp_socket

    什么是WinSock?

    这还不简单,当然是Windows的袜子啦

    WinSock(全称Windows Sockets)是巨硬微软提供的用于Windows平台的C++网络连接库。

    简单来说我们知道在Java中,我们可以通过JDK提供的java.net库来实现建立套接字的TCP/UDP传输:

    public class Client
    {
    	private String svrip = "";
    	private int port;
    	
    	public AnotherClient(String serverIP,int port) {
    		svrip = serverIP;
    		this.port = port;
    	}
    	
    	public void Start() {
    		Socket cltsock = null; // Client's socket
    		
    		OutputStream ostrm = null; // O-stream for sending to server
    		BufferedWriter bwriter = null;
    		
    		InputStream istrm = null;
    		BufferedReader breader = null;
    		
    		try {
    			cltsock = new Socket(svrip,port);
    			System.out.println(String.format("Client at %s has started.", cltsock.getInetAddress().toString()));
    			istrm = cltsock.getInputStream();
    			
    			// .....后面不写了,意思意思得了
    

    WinSock的功能与之类似,只不过因为结构僵化的C++标准委员会迟迟不将专用于C++网络连接的<network>纳入STL(标准库)中,所以我们这里只好委曲求全地使用Windows的标准,在Linux环境中,替代品为系统内核提供的<sys/socket.h>库。不过整体使用上,两者区别并不那么大。

    Winsock比较常用的有两类版本——1.1和2.x。本文这里,当然了,标题也说了,以Winsock2为准,建立一个IPv4协议下的传输链路

    Workflow

    嗯,自从因为各种原因对自己产生了一个巨大的否定以来,我发现总结一个具体的工作流程(Workflow)出来还是挺重要的一件事。

    那么,我们这里就尝试总结一下,建立一个连接并发送消息期间,Winsock都做了什么:

    graph TD subgraph server[Server] begin[开始] --> init[初始化WSA] --> setupsvr[建立Server Socket] --> binding[设置地址绑定] --> listening[Server Socket启动监听] accept((接收来自Client的连接)) recv[接收来自Client的消息] send[向Client发送信息] close[关闭Server Socket] terminal[结束] listening --> accept accept --> send accept --> recv send --> close recv --> close close --> terminal end subgraph client[Client] beginc[开始] --> initc[初始化WSA] --> setupclt[建立Client Socket] connect{连接ip所在的Server的连接} crecv[接收来自Server的消息] csend[向Server发送信息] cclose[关闭Client Socket] terminalc[结束] setupclt --> connect connect --> csend connect --> crecv csend --> cclose crecv --> cclose cclose --> terminalc end csend -.-> recv send -.-> crecv

    Server Side(服务端)

    先说Server这边,从上面这张图来看,他做了这么几件事:

    1. 初始化WSA
      所谓WSA就是WinSock API,当然了,因为我们这里用的是Winsock2,所以我们做的就是对Winsock2的初始化,毕竟只有初始化之后我们才能使用Winsock里面的相关功能,当然具体来说这里其实还包括一些其他的事情,比如版本比对之类的。

    2. 建立Server的套接字
      当我们可以使用Winsock的相关功能后,我们就可以通过Winsock建立套接字以建立连接了。

    3. 绑定地址
      这个先留着,放到代码里再说……

    4. 启动监听
      当套接字被启动后,我们允许该套接字监听是否有外部的客户端的连接请求

    5. 接收Client的连接
      有Client的时候我们获得这位Client的套接字。

    6. 收发消息
      连接建立完成了,可以传输数据了

    7. 结束
      打完收工!

    初始化WSA

    既然我们使用Winsock2,那么我们首先做的必然就是对Winsock2的调取,原始的来说,Winsock2隶属于Win32的系统库,当你引入头文件WinSock2.h的时候你就已经获得了Winsock2的结构声明。当然,为了让他能够调取系统库,我们需要把他静态链接进来

    #include <WinSock2.h>
    #include <ws2tcpip.h>	//???
    #pragma comment(lib, "WS2_32.lib")
    

    注意,上面第2行中,我又引入了一个ws2tcpip.h,这是因为在Winsock2.h中声明的有关函数不再倡用了(Deprecated),这种情况下直接使用这类函数会直接触发错误,当然,如果你执意要用的话,那就在最开始的时候补充一下抑制宏

    //如果你非要使用Deprecated内容的话
    #define _WINSOCK_DEPRECATED_NO_WARNINGS
    // includes... pragma...
    

    然后我们开始在Server的主程序里初始化WSA,初始化过程主要经过:设置版本限制→初始化→获得初始化信息,因此我们首先设置我们能接受的WinSock库的最低版本:

    WORD verRequest = MAKEWORD(1, 1);
    

    虽然说我们用的是WinSock2,不过我们这里本着向下兼容的原则,我们约定要是只有1.1也行……
    然后我们需要使用WSAStartup函数来真正初始化WSA,这个函数接收两个参数,一个版本约定和一个数据结构:

    WSAData wData;
    int $err = WSAStartup(wVersionRequest, &wData);
    

    初始化期间,系统会根据你提供的Version Request来评估当前系统内的Winsock版本,并将结果写进wData中,返回初始化失败的错误代码,如果没有错误返回0,因此,这里我们在发生错误的情况下直接结束程序。

    if($err != 0)
    {
    	cout << "Initialization Failed" << endl;
    	WSACleanup();
    	ExitProcess($err);
    }
    

    如果到这里没进入if的话,那么说明初始化成功了,我们可以输出WSA的信息看一眼:

    cout << wData.szDescription << endl;
    

    wData.szDescription存放了初始化时系统获得的API描述文本,在我的机器上,输出的是

    WINSOCK 2.0
    

    很明显,因为我用的WinSock2嘛,也就是说现在为止我系统中提供的WinSock库并没有什么问题。

    建立服务端套接字

    既然WSA可以正常使用,那么从这里开始我们就正式的开始用WSA提供的功能了。

    既然是服务端程序,那么能想到的第一件事必然是:建立套接字(Socket)

    WinSock提供了SOCKET类型和socket函数来表示并创建一个套接字。我们先来看一下socket()函数该怎么用:

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

    其实前面修饰符还有什么_Must_inspect_result_什么的,这些我们先不管,捞干的来看就是这样的函数。

    参数名称 类型 用途
    af int 你的socket所使用的地址协议族,我们这里只介绍IPv4的,所以这里填入AF_INET就好了
    type int 你的socket建立连接所传输的数据形式,比较常用的有流式SOCK_STREAM或者数据报SOCK_DGRAM,当然还有其他的这里暂时不作介绍
    protocol int 你的socket使用的传输协议,可填入以IPPROTO_*开头的常量,也可以填入0来自动选择协议,自动选择协议的规则与type相关,例如type=SOCK_STREAM那么会使用TCP协议,如果是type=SOCK_DGRAM那么会选择UDP协议

    我们这里以IPv4的TCP协议为例,那么可以创建这样的socket:

    SOCKET sckSrv = socket(AF_INET, SOCK_STREAM, 0);
    

    如果socket没有成功创建,那么函数会返回一个INVALID_SOCKET

    为Server socket绑定地址

    使用上面的方法创建的套接字仅包含地址协议的信息,但这个套接字并不具备一个地址,出于某种原因,我们并不能直接操纵Socket实体本身来设置这些东西,因为SOCKET类型说到底只是一个句柄id,并不承载其他信息。WinSock提供了专用于给socket绑定地址的函数bind

    int bind(
    	SOCKET		s,
    	const sockaddr 	*name,
    	int            	namelen
        );
    

    还是直接捞干的看,这里的SOCKET s肯定就是刚才的sckSrv,填进去就可以了。而name这里就需要特别注意一下了,name要求你输入的实际上是你的ip地址,但是这里接受的类型是SOCKADDR *SOCKADDR是用于表示地址的一个数据结构,包含地址协议族和具体的ip地址信息,不过SOCKADDR的结构很raw,不是很好构建,因此我们需要使用一个改进结构SOCKADDR_IN,这个结构专用于IPv4(IPv6请使用SOCKADDR_IN6):

    typedef struct sockaddr_in {
            short   sin_family;
            u_short sin_port;
            struct  in_addr sin_addr;
            char    sin_zero[8];
    } SOCKADDR_IN;
    

    因此我们这里如果要构建地址的话:

    SOCKADDR_IN addrSrv;
    addrSrv.sin_family = AF_INET;
    

    注意,这里的sin_addr是个in_addr类型,这个类型并不支持直接输入我们所说的字面上的ip地址"xxx.xxx.xxx.xxx"这种的,因此如果你想用字符串的ip地址输入进去的话,就必须用inet_aton做地址形式转换。

    #ifdef _WINSOCK_DEPRECATED_NO_WARNINGS
    //指定为本机地址
    addrSrv.sin_addr = inet_addr("127.0.0.1");
    //或者你也可以指定为缺省路由(0.0.0.0),向下面这样写
    addrSrv.sin_addr = INADDR_ANY;
    #endif
    

    然而,如果你没有解除Deprecation warning(没有定义_WINSOCK_DEPRECATED_NO_WARNINGS)的话,那么直接使用inet_addr报错,这种情况下如果坚持不添加抑制宏的替代做法是使用inet_pton来完成:

    #if (!defined _WINSOCK_DEPRECATED_NO_WARNINGS) && (defined _WS2TCPIP_H_)
    inet_pton(AF_INET, "127.0.0.1", &(addrSrv.sin_addr));
    #endif
    

    其实这个类型是个32位整数,如果你确定你的ip地址写成32位整数是什么样子的话那么没有必要费劲巴力的做地址形式转换,而直接赋值成0x????????的形式,但是这样代码可读性很差,而且还涉及到本机到网络传输时的大小端点转换问题(hton*),而inet_*转换出来的地址码是确定符合网络传输的形式(这话是巨硬msdn里说的)。

    最后我们不要忘了还得提供端口号:

    addrSrv.sin_port = htons(17500);	// 这个需要转换为网络格式,端口号随你喜欢
    

    当然,如果你引入了<ws2tcpip.h>,那么你还可以继续用更加通用的地址结构sockaddr_gen,当然,不用也没关系,使用这个结构(其实是个union)只是规避了接下来的类型转换的问题。(注意,名字是小写的,和之前的不一样,之前的SOCKADDRSOCKADDR_IN其实也可以全小写):

    #ifdef _WS2TCPIP_H_
    sockaddr_gen gaddrSrv;
    gaddrSrv.AddressIn = addrSrv;
    

    上述内容其实是为了构建地址(不得不说确实挺麻烦,而且事情特别多,但是如果理清的话其实很容易),说白了就是准备填入name这个参数,但是注意,由于bindname参数是SOCKADDR *类型,因此你不管用什么类型折腾,最后都要转换回这个raw类型:

    // 如果没定义gaddrSrv
    bind(sckSrv, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR));
    // 如果定义了gaddrSrv
    bind(sckSrv, &(gaddrSrv.Address), sizeof(SOCKADDR));
    

    第三个参数的namelen就指定成SOCKADDR的大小即可(因为name指向SOCKADDR嘛)。

    启动服务端监听

    服务端嘛,你要服务的嘛,所以我们需要打开服务端的监听,让sckSrv听取外界是否有其他socket连入,使用listen函数启动监听:

    listen(sckSrv, SOMAXCONN);
    

    第一个参数就是你的服务器套接字,第二个参数是最大允许的连接等待队列长度,SOMAXCONN是最大的允许限制范围了,设置为SOMAXCONN可以理解为没有数量限制(其实是有的,SOMAXCONN=0x7fffffff,当然,WinSock允许使用SOMAXCONN_HINT设置更大的值,但仅限于能够在巨硬自己的TCP/IP协议服务供应器下使用,我们这里不考虑这个东西)。

    获取连入的客户端

    启用监听后,Server将能够获得来自外界的连接请求,使用accept原语来取得连入的客户端信息:

    SOCKET accept(
      SOCKET   s,
      sockaddr *addr,
      int      *addrlen
    );
    

    accept原语令服务器s接收第一个等待连接的客户端(如果没有则阻塞这个服务器进程,直至有第一个进入),并获取这个客户端的地址信息,存入addr中。

    由于我们后面会需要Server向Client发送数据,因此我们这里获得客户端addr的行为是必需的,所以还是类似的方式,定义客户端的addr结构:

    SOCKADDR_IN addrCli;
    SOCKET sckCli = accept(sckSrv, (SOCKADDR *) &addrCli, sizeof(SOCKADDR));
    

    由于我们并不知道客户端的任何网络地址信息,因此我们只创建addrCli但无需初始化,与addrSrv类似,你也可以使用sockaddr_gen类型,这里不演示了。

    再次强调,这里的accept原语是阻塞的!如果需要非阻塞的accept原语,可能需要select配合,但是本文暂时不讨论这个。

    当然,如果accept的参数不正确(比如你的addrlen大小不对),那么他有可能返回一个INVALID_SOCKET

    收发数据

    获得了客户端的socket,我们就说建立了从server到socket的连接,连接建立完成,我们就可以经由这个连接传递数据了(双向地)。

    当然了,直接使用sendrecv原语就可以了,但是在使用之前,我们先确立缓冲区用于存储收发的数据:

    char sendbuf[1024] = "Hello, from SERVER.",
    	 recvbuf[1024];
    

    然后我们集中看一下send和recv原语:

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

    应该很言简意赅了,s是你要发送的client socket,flags我们先不管,给0就可以,flags主要控制消息收发时的行为,我们这里就按照一般的收发方法正常收发就可以了。我们这里让server先对连入的client发送消息,然后再发送(留意这一点,下面我们写客户端的时候就需要把这个顺序反过来,当然你也可以在这里先收后发,那在客户端那边就还得反过来):

    send(sckCli, sendbuf, sizeof(sendbuf), 0);
    recv(sckCli, recvbuf, sizeof(recvbuf), 0);
    

    就可以收发信息了,如果没有别的事情的话,那么就可以……

    关闭套接字、释放WSA

    善始善终是一种美德,特别是对于C++而言。如果没有别的事情,我们想结束的话,那就需要关闭套接字,然后释放掉WSA,这两个很简单:

    closesocket(sckCli);
    closesocket(sckSrv);
    WSACleanup();
    ExitProcess(0);
    

    好了,服务端程序就写的差不多了,代码会在文章最后整理。

    Client Side(客户端)

    客户端和服务端的写法就差不太多了,但是注意几点:

    1. 由于客户端是积极连接服务端,因此客户端一般不需要自己的ip地址,所以客户端的socket不需要地址绑定,也不需要启动监听
    2. 服务端是accept原语,那么客户端就是connect原语
    3. 客户端只需要创建自己的socket,不需要考虑服务端
    4. 没了

    相比较刚才的过程而言,再补充说明几个新加入的东西……

    与服务端的积极连接

    服务端是被动的accept外界的连接,那等的是谁呢,显然就是客户端主动的connect。

    int connect(
      SOCKET         s,
      const sockaddr *name,
      int            namelen
    );
    

    其中s客户端自己的socket,虽然说客户端的socket并不需要绑定地址,但是客户端仍然要提供服务端的地址信息(不然鬼知道你想跟谁连),也就是说addrSrv仍然要提供。

    然后再注意的一点就是收发和服务端应当是相反的

    同样,代码我在后面汇总。

    当你遇到错误时……

    写这两个程序期间,你可能就已经开始运行调试了,当然多数时候你不大可能一遍写成(除非你是直接拷的代码),因此难以避免的你会遇到各种错误,有的和你的代码有关,有的可能与你的网络环境有关,前者你大可通过调试器和文档来解决,但后者就很难捕捉了。

    而WinSock用于返回错误的函数WSAGetLastError(),主要用于返回错误代码,你可以根据msdn文档对照你的错误代码来定位你的错误。如果没有任何错误,该函数应当返回0。

    代码汇总

    Server

    #include <WinSock2.h>
    #include <ws2tcpip.h>	//???
    #include <iostream>
    
    #pragma comment(lib, "WS2_32.lib")
    
    using namespace std;
    
    int main(int argc, char **argv)
    {
    	WORD verRequest = MAKEWORD(1, 1);
    	WSAData wData;
    	int $err = WSAStartup(wVersionRequest, &wData);
    	if($err != 0)
    	{
    		cout << "Initialization Failed" << endl;
    		WSACleanup();
    		ExitProcess($err);
    	}
    	cout << wData.szDescription << endl;
    
    	SOCKET sckSrv = socket(AF_INET, SOCK_STREAM, 0);
    	if(sckSrv == INVALID_SOCKET)
    	{
    		closesocket(sckSvr);
    		WSACleanup();
    		ExitProcess(INVALID_SOCKET);
    	}
    
    	SOCKADDR_IN addrSrv;
    	addrSrv.sin_family = AF_INET;
    	inet_pton(AF_INET, "127.0.0.1", &(addrSrv.sin_addr));
    	addrSrv.sin_port = htons(17500);
    
    	bind(sckSrv, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR));
    
    	listen(sckSrv, SOMAXCONN);
    
    	SOCKADDR_IN addrCli;
    	SOCKET sckCli = accept(sckSrv, (SOCKADDR *) &addrCli, sizeof(SOCKADDR));
    	
    	if(sckCli == INVALID_SOCKET)
    	{
    		closesocket(sckCli);
    		closeSocket(sckSvr)
    		WSACleanup();
    		ExitProcess(INVALID_SOCKET);
    	}
    
    	char sendbuf[1024] = "Hello, from SERVER.",
    	 	 recvbuf[1024];
    
    	send(sckCli, sendbuf, sizeof(sendbuf), 0);
    	recv(sckCli, recvbuf, sizeof(recvbuf), 0);
    	
    	cout << recvbuf << endl;
    
    	closesocket(sckCli);
    	closesocket(sckSrv);
    
    	WSACleanup();
    	
    	ExitProcess(0);
    }
    

    Client

    #include <WinSock2.h>
    #include <ws2tcpip.h>	//???
    #include <iostream>
    
    #pragma comment(lib, "WS2_32.lib")
    
    using namespace std;
    
    int main(int argc, char **argv)
    {
    	WORD verRequest = MAKEWORD(1, 1);
    	WSAData wData;
    	int $err = WSAStartup(wVersionRequest, &wData);
    	if($err != 0)
    	{
    		cout << "Initialization Failed" << endl;
    		WSACleanup();
    		ExitProcess($err);
    	}
    	cout << wData.szDescription << endl;
    
    	SOCKET sckCli;
    	if(sckCli == INVALID_SOCKET)
    	{
    		closesocket(sckCli);
    		WSACleanup();
    		ExitProcess(INVALID_SOCKET);
    	}
    
    	SOCKADDR_IN addrSrv;
    	addrSrv.sin_family = AF_INET;
    	inet_pton(AF_INET, "127.0.0.1", &(addrSrv.sin_addr));
    	addrSrv.sin_port = htons(17500);
    
    	connect(sckCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
    
    	char sendbuf[1024] = "Hi, from CLIENT.",
    	 	 recvbuf[1024];
    
    	recv(sckCli, recvbuf, sizeof(recvbuf), 0);
    	cout << recvbuf << endl;
    	send(sckCli, sendbuf, sizeof(sendbuf), 0);
    
    	closesocket(sckCli);
    
    	WSACleanup();
    
    	ExitProcess(0);
    }
    

    作者:Oberon
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Nginx配置图片请求
    Nginx 配置浏览Linux 系统目录并下载文件
    SpringBoot + Dubbo + Zookper 整合
    mysql 随机选取一条符合条件的记录
    linux 安装rabbitMQ详细教程
    spring boot 实现redis 的key的过期监听,执行自己的业务
    springboot 配置将info、error、debug 分别输出到不同文件
    使用 mvn install 命令将本地jar包注册到本地maven仓库
    关于Snowflake 生成53位ID
    spring boot 或 spring 集成 atomikos jta 完成多数据源事务管理
  • 原文地址:https://www.cnblogs.com/oberon-zjt0806/p/14814144.html
Copyright © 2020-2023  润新知