本文地址: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 *) &sockaddr, 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 *) &sockaddr, 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);
}

Winsock2使用记录的更多相关文章

  1. 【原】开发路上疑难BUG调试记录

    之前遇到棘手的BUG总是在处理过后就不管了,导致后面碰到相同问题后重复工作太多.现专门开辟一篇日志以记录接下来一路上比较棘手的“坑”的修复历程: [C++篇] 1.mt.exe : general e ...

  2. mingw 构建 mysql-connector-c-6.1.9记录

    1.准备工作 首先需要下载mysql-connector-c-6.1.9的源码,然后解压. 然后需要准备编译环境,这里我使用的是msys2(下载地址http://repo.msys2.org/dist ...

  3. 记一次debug记录:Uncaught SyntaxError: Unexpected token ILLEGAL

    在使用FIS3搭建项目的时候,遇到了一些问题,这里记录下. 这里是发布搭建代码: // 代码发布时 fis.media('qa') .match('*.{js,css,png}', { useHash ...

  4. nginx配置反向代理或跳转出现400问题处理记录

    午休完上班后,同事说测试站点访问接口出现400 Bad Request  Request Header Or Cookie Too Large提示,心想还好是测试服务器出现问题,影响不大,不过也赶紧上 ...

  5. Kali对wifi的破解记录

    好记性不如烂笔头,记录一下. 我是在淘宝买的拓实N87,Kali可以识别,还行. 操作系统:Kali 开始吧. 查看一下网卡的接口.命令如下 airmon-ng 可以看出接口名称是wlan0mon. ...

  6. 2015 西雅图微软总部MVP峰会记录

    2015 西雅图微软总部MVP峰会记录 今年决定参加微软MVP全球峰会,在出发之前本人就已经写这篇博客,希望将本次会议原汁原味奉献给大家 因为这次是本人第一次写会议记录,写得不好的地方希望各位园友见谅 ...

  7. 分享一个SQLSERVER脚本(计算数据库中各个表的数据量和每行记录所占用空间)

    分享一个SQLSERVER脚本(计算数据库中各个表的数据量和每行记录所占用空间) 很多时候我们都需要计算数据库中各个表的数据量和每行记录所占用空间 这里共享一个脚本 CREATE TABLE #tab ...

  8. 我是如何在SQLServer中处理每天四亿三千万记录的

    首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. ...

  9. 前端学HTTP之日志记录

    前面的话 几乎所有的服务器和代理都会记录下它们所处理的HTTP事务摘要.这么做出于一系列的原因:跟踪使用情况.安全性.计费.错误检测等等.本文将谥介绍日志记录 记录内容 大多数情况下,日志的记录出于两 ...

随机推荐

  1. Day13_70_join()

    join() 方法 * 合并线程 join()线程合并方法出现在哪,就会和哪个线程合并 (此处是thread和主线程合并), * 合并之后变成了单线程,主线程需要等thread线程执行完毕后再执行,两 ...

  2. Linux(CentOS7)安装与卸载MySQL8.0图文详解

    Mysql数据库的安装对于开发者来说,是我们必然会面对的问题,它的安装过程其实并不复杂,并且网络上的安装教程也非常多,但是对于新手来说,各种不同形式的安装教程,又给新手们带来了要选择哪种方式进行安装的 ...

  3. Manachar's Algorithm

    1.模板 1 #include<bits/stdc++.h> 2 using namespace std; 3 const int MAX=21000020; 4 char s[MAX], ...

  4. git Windows下重命名文件,大小写敏感问题

    作为一个重度强迫症患者,是不忍受文件名,有字母大小拼写错误的,但是在git下,已是受控版本文件要改过来,要费些周章了. 一.环境 Widnows + git version 2.24.0 + Tort ...

  5. 功能:SpringBoot整合rabbitmq,长篇幅超详细

    SpringBoot整合rabbitMq 一.介绍 消息队列(Message Queue)简称mq,本文将介绍SpringBoot整合rabbitmq的功能使用 队列是一种数据结构,就像排队一样,遵循 ...

  6. 感染性的木马病毒分析之样本KWSUpreport.exe

    一.病毒样本简述 初次拿到样本 KWSUpreport_感染.exe.v 文件,通过使用PE工具,并不能辨别出该样本是那种感染类型,使用了一个比较直接的方法,从网上查资料,获取到了该样本的正常EXE文 ...

  7. Windows核心编程 第2 5章 未处理异常和C ++异常(上)

    未处理异常和C + +异常(上) 前一章讨论了当一个异常过滤器返回 E X C E P T I O N _ C O N T I N U E _ S E A R C H时会发生什么事情.返回EXCEPT ...

  8. 【】POST、GET、RequestParam、ReqestBody、FormData、request payLoad简单认知

    背景: 使用vue+axios方式代替ajax后向后台发送数据出现问题了,controller获取不到数据.然后查.找.查.找中似乎找到一些门道.以下列出总结性的东西来记录自己的思考成果,仅供参考,不 ...

  9. 分享一个PHP登录小妙招

    待完善 思想参照fastadmin api 文件路径 /fastadmin/application/common/library/Auth.php->login().logout().isLog ...

  10. 多线程-5.JMM之happens-before原则

    a happens-before b 翻译为a操作对b操作是可见的.可见即是指共享变量的更改能获知. 特性:传递性 原则:volatile定义的变量 写操作 happens-before 读操作 同一 ...