01.最简单的通信模型

Posted oldmao_2001

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了01.最简单的通信模型相关的知识,希望对你有一定的参考价值。


试看:https://www.bilibili.com/video/BV1cb411w7sZ?p=1
购买:https://study.163.com/course/introduction.htm?courseId=1006358018
先试看一下,做点笔记,当然还会结合之前教过的网络编程知识进行补充。
作者是C3程序猿
以下简介搬运之原课程介绍
课程章节:
第一章 c/s模型的讲解以及代码实现
第二章 select模型的讲解以及代码实现
第三章 异步选择模型的讲解以及代码实现
第四章 事件选择模型的讲解以及代码实现
第五章 重叠i/o模型的讲解以及代码实现
第六章 完成端口模型的讲解以及代码实现
第七章 tcp/ip基础知识的讲解,包括网络分层,三次握手,四次挥手,协议头等等。
语言是C语言
没装VS 2013,装了VC6.0
网络编程主要学习:TCP/IP,UDP,HTTP即可,其实前面两个还比较像,HTTP没认真学过,不发表意见。
参考书:windows网络编程(第一版)罗莉琴
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
https://tangentsoft.net/wskfaq/

基本概念

网络编程:从理论上看是基于网络协议的编程;从代码角度上看,就是就是调用对应的函数,传递对应的参数。
网络协议:协议即规则,网络协议就是双方通信的规则。
协议就好比语言,要想和中国人打交道就要讲中国话,和美国人讲话就要讲英语,这个就好比TCP这些协议,客户机要和服务器进行通讯,就要使用相同协议。中国话里面有有细分的地方话,就好比有基于TCP的SNTP协议。
TCP/IP:是一个协议家族不是面向连接的,可靠的,基于字节流的传输层协议。
UDP:面向非连接的,不可靠的,基于数据报的传输层协议。
套接字编程=Socket编程=网络编程
Socket实际上有分Windows(WinSock)和Linux(BSD Socket),二者接口函数名称虽然一致,但是WinSock增加了一些扩展函数。

头文件和库文件

不用问就是每次要写下面两句话:
下面例子没报错说明头文件和库文件没问题,本来不想装VC的,机器上有CFREE,但是CFREE没有这两个文件,网上找一圈居然没有下载,不得已又装上了许久不用的VC6.0

# include <WinSock2.h>
# pragma comment(lib, "Ws2_32.lib")

int main(void)
{

	return 0;
}

当然,类似MFC,WPF有自己封装好的网络库,但是最底层的还是上面两个文件,当然我们也可以自己进行封装。 <WinSock2.h>的最新版本是2.2,比<WinSock.h>内容要多,但是和BSD Socket差别也比较大,如果要想把Linux下的BSD Socket移植到Windows来,可以用<WinSock.h>。
库文件名里面有32,但是无论是64还是32位系统都是用它。
头文件和库文件文件名不区分大小写
下面先来看个例子

服务端

1.打开网络库

int WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);

命令中的WSA三个字母分别对应:Windows、Socket、Asynchronous(同步:阻塞状态,当然还有异步工作模式非阻塞模式),Startup代表启动。启动了这个库,这个库里的函数/功能才能使用。
https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsastartup
wVersionRequired:指定系统支持的Socket版本库,可以用MAKEWORD来设置。

WORD wdVersion=MAKEWORD(2,2);//2.2
int a=*((char*)&wdVersion); //看低位
int b=*((char*)&wdVersion+1);//看高位

这里的WORD类型共两个八位字节,低八位(地址小)存主版本号就是·前面那个2,高八位(地址大)存副版本号就是点后面那个2,写成二进制就是:
00000010 低 八 位 00000010 高 八 位 = 514 \\underset{低八位}{0000 0010 }\\quad\\underset{高八位}{0000 0010}=514 0000001000000010=514
可以调试看看,整个表示为十进制是514。
具体操作可以看MAKEWORD的源码:

#define MAKEWORD(a, b)      ((WORD)(((BYTE)(a)) | ((WORD)((BYTE)(b))) << 8))

就是把传进来主版本号的左移八位,空出高八位的八个0
((WORD)((BYTE)(b))) << 8 = 00000010 低 八 位 00000000 高 八 位 \\text{((WORD)((BYTE)(b))) << 8}=\\underset{低八位}{0000 0010 }\\quad\\underset{高八位}{0000 0000} ((WORD)((BYTE)(b))) << 8=0000001000000000
然后再和主版本号进行按位或就得到主版本号在低八位,副版本号在高八位的结果。
当输入版本不存在:

输入描述结果
输入1.3、2.65有主版本,没有副版本得到该主版本的最大副版本1.1、2.2并使用
输入4.0超过最大版本号使用系统能提供的最大的版本
输入0.5缺少主版本号网络库打开失败,不支持请求的套接字版本

lpWSAData:是一个WSADATA结构体指针


lp开头的参数都是要传入指针,或者一个地址。例如LPWSADATA 就是要WSADATA的地址或者指针作为参数传入。
从头文件里面可以看到LPWSADATA和WSADATA*是一个玩意,是个别名


typedef struct WSAData {
        WORD                    wVersion;
        WORD                    wHighVersion;
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
        unsigned short          iMaxSockets;
        unsigned short          iMaxUdpDg;
        char FAR *              lpVendorInfo;
} WSADATA, FAR * LPWSADATA;

如果用LPWSADATA或者WSADATA*来定义WSAStartup参数的话,那么就要为其malloc申请空间,在程序后面要free,貌似很麻烦,一般用后面一种方法。

LPWSADATA lpw = (WSADATA*)malloc(sizeof(WSADATA));
WSAStartup(wdVersion,lpw);
free(lpw);

如果直接用WSADATA,那么在传入参数的时候,直接&取地址就可以:

WSADATA wdScokMsg;
WSAStartup(wdVersion,&wdScokMsg);

函数返回值为0表示成功,
在这里插入图片描述
wVersion:我们要使用的版本,就是自己初始化MAKEWORD那里指定的版本
wHighVersion:系统能提供给我们最高的版本
iMaxSockets:返回可用的socket的数量,2.0版本之后就没用了
iMaxUdpDg:UDP数据报信息的大小,2.0版本之后就没用了
lpVendorlnfo:供应商特定的信息,2.0版本之后就没用了
zDescription:当前库的描述信息,2.0是第二版的意思
szSystemstatus :Running表示正在运行

否则返回错误码,可以写一段代码判断是否执行成功。具体内容可以看CSDN。

	#include <stdio.h>
	int nRes = WSAStartup(wdVersion,lpw);

	if (0 != nRes)
	{
		switch(nRes)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启。。。");
				break; 
			case WSAVERNOTSUPPORTED: 
				printf("解决方案:更新网络库");
				break; 
			case WSAEINPROGRESS: 
				printf("解决方案:重启。。。");
				break; 
			case WSAEPROCLIM: 
				printf("解决方案:网络连接达到上限或阻塞,关闭不必要软件");
				break; 
			case WSAEFAULT:
				printf("解决方案:程序有误");
				break;
		}
		return 0;
	}
名称数值含义解决方案
WSASYSNOTREADY10091底层网络子系统尚未准备好进行网络通信。重启电脑,并检查库文件是否存在
WSAVERNOTSUPPORTED10092此特定Windows套接字实现不提供所请求的Windows套接字支持的版本。指定版本不支持,只能换低版本的试试
WSAEPROCLIM10067已达到对Windows套接字实现支持的任务数量的限制。Windows Sockets实现可能限制同时使用它的应用程序的数量
WSAEINPROGRESS10036正在阻止Windows Sockets 1.1操作。当前函数运行期间,由于某些原因造成阻塞,会返回该错误码,其他操作均禁止
WSAEFAULT10014IpWSAData参数不是有效指针。参数写错了

当然还有别的错误码:


/*
 * Extended Windows Sockets error constant definitions
 */
#define WSASYSNOTREADY          (WSABASEERR+91)
#define WSAVERNOTSUPPORTED      (WSABASEERR+92)
#define WSANOTINITIALISED       (WSABASEERR+93)
#define WSAEDISCON              (WSABASEERR+101)
#define WSAENOMORE              (WSABASEERR+102)
#define WSAECANCELLED           (WSABASEERR+103)
#define WSAEINVALIDPROCTABLE    (WSABASEERR+104)
#define WSAEINVALIDPROVIDER     (WSABASEERR+105)
#define WSAEPROVIDERFAILEDINIT  (WSABASEERR+106)
#define WSASYSCALLFAILURE       (WSABASEERR+107)
#define WSASERVICE_NOT_FOUND    (WSABASEERR+108)
#define WSATYPE_NOT_FOUND       (WSABASEERR+109)
#define WSA_E_NO_MORE           (WSABASEERR+110)
#define WSA_E_CANCELLED         (WSABASEERR+111)
#define WSAEREFUSED             (WSABASEERR+112)

具体可以看:https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2

2.校验版本

这里可以用两个宏:
HIBYTE是获取高位副版本
LOBYTE是获取低位主版本

	//校验版本,只要有一个不是2,说明系统不支持我们要的2.2版本	
	if (2!=HIBYTE(lpw->wVersion)|| 2!=LOBYTE(lpw->wVersion))
	{
			printf("版本有问题!");
			WSACleanup();//关闭网络库
			return 0;
	}

如果WSAStartup用的地址就是:

	WSADATA wdScokMsg;
	WSAStartup(wdVersion,&wdScokMsg);
	//校验版本,只要有一个不是2,说明系统不支持我们要的2.2版本
	if (2!=HIBYTE(wdScokMsg.wVersion)|| 2!=LOBYTE(wdScokMsg.wVersion))
	{
			printf("版本有问题!");
			WSACleanup();//关闭网络库
			return 0;
	}

3.创建SOCKET

SOCKET背景知识

概念:将底层复杂的协议体系,执行流程,进行了封装,封装完的结果,就是一个SOCKET了,也就是说,SOCKET是调用协议进行通信的操作接口。SOCKET将复杂的协议过程与我们编程人员分开,我们直接操作一个简单SOCKET就行了,对于底层的协议过程细节,完全不用知道(协议本身种类繁多,复杂性高,封装后编程就不用考虑这么多了)。
SOCKET的本质就是一个唯一的整数(uint),可以看下定义,在Windows中称为句柄。
整个网络编程的所有函数都要用到这个句柄,所以网络编程=Socket编程。

代码

不同协议创建(实例化)的SOCKET代码稍微有点不一样,TCP的SOCKET实例化如下所示:

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

参数1:地址类型
下表中的后面2个VC 6.0还不支持。。。
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket

名称取值含义
AF_INET2Internet协议版本4(IPv4)地址系列。
AF_INET623Internet协议版本6(IPv6)地址系列。
AF_BTH32蓝牙地址系列。如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。
AF_IRDA26红外数据协会(IrDA)地址系列。仅当计算机安装了红外端口和驱动程序时,才支持此地址系列。
#define AF_UNIX         1               /* local to host (pipes, portals) */
#define AF_INET         2               /* internetwork: UDP, TCP, etc. */
#define AF_IMPLINK      3               /* arpanet imp addresses */
#define AF_PUP          4               /* pup protocols: e.g. BSP */
#define AF_CHAOS        5               /* mit CHAOS protocols */
#define AF_NS           6               /* XEROX NS protocols */
#define AF_IPX          AF_NS           /* IPX protocols: IPX, SPX, etc. */
#define AF_ISO          7               /* ISO protocols */
#define AF_OSI          AF_ISO          /* OSI is ISO */
#define AF_ECMA         8               /* european computer manufacturers */
#define AF_DATAKIT      9               /* datakit protocols */
#define AF_CCITT        10              /* CCITT protocols, X.25 etc */
#define AF_SNA          11              /* IBM SNA */
#define AF_DECnet       12              /* DECnet */
#define AF_DLI          13              /* Direct data link interface */
#define AF_LAT          14              /* LAT */
#define AF_HYLINK       15              /* NSC Hyperchannel */
#define AF_APPLETALK    16              /* AppleTalk */
#define AF_NETBios      17              /* NetBios-style addresses */
#define AF_VOICEVIEW    18              /* VoiceView */
#define AF_FIREFOX      19              /* Protocols from Firefox */
#define AF_UNKNOWN1     20              /* Somebody is using this! */
#define AF_BAN          21              /* Banyan */
#define AF_ATM          22              /* Native ATM Services */
#define AF_INET6        23              /* Internetwork Version 6 */
#define AF_CLUSTER      24              /* Microsoft Wolfpack */
#define AF_12844        25              /* IEEE 1284.4 WG AF */


#define AF_MAX          26

参数2:套接字类型(数据的传递方式)

名称数值内容
SOCK_STREAM1提供带有OOB数据传输机制的顺序,可靠,双向,基于连接的字节流。此套接字类型使用传输控制协议(TCP)作为Internet地址系列(AF_INET或AF_INET6)。
SOCK_DGRAM2一种支持数据报的套接字类型,它是固定(通常很小)最大长度的无连接,不可靠的缓冲区。此套接字类型使用用户数据报协议(UDP)作为Internet地址系列(AF INET或AF_INET6)。
SOCK_RAW3提供允许应用程序操作下一个上层协议头的原始套接字。要操作lPv4标头,必须在套接字上设置IP_HDRINCL套接字选项。要操作lPv6标头,必须在套接字上设置IPV6_HDRINCL套接字选项。
SOCK_RDM4提供可靠的消息数据报。这种类型的一个示例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目。仅在安装了可靠多播协议时才支持此类型值。
SOCK_SEQPACKET5提供基于数据报的伪流数据包。

参数3:协议类型,常见搭配如下表所示:

名称数值协议名称地址参数套接字类型
IPPROTO_TCP6传输控制协议(TCP)AF_INET或AF_INET6SOCK_STREAM
IPPROTO_UDP17用户数据报协议(UDP)AF_INET或AF INET6SOCK_DGRAM
IPPROTO_ICMP1Internet控制消息协议(ICMP)AF UNSPEC,AF_INET或AF_INET6SOCK RAW或未指定
IPPROTO_IGMP2Internet组管理协议(IGMP)AF UNSPEC,AF_INET或AF_INET6SOCK RAW或未指定
IPPROTO_RM113用于可靠多播的PGM协议(Windows Vista以上版本叫IPPROTO_PGM)AF_INETSOCK_RDM(仅在安装了可靠多播协议时才支持此协议值)

注意:协议类型直接写0默认是TCP协议,但是尽量避免这样写,以免以后的代码升级出BUG。

返回值:
成功则返回Socket句柄,使用完毕要用CloseSocket(socketListen)销毁该句柄。
失败返回INVALID_SOCKET,此时要关闭网络库,可用WSAGetLasterror()返回错误码。此时不用关闭Socket句柄。

if(INVALID_SOCKET == socketServer)
	{
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	closesocket(socketServer);//如果有创建客户端SOCKET句柄,也要关闭
	WSACleanup();

4.绑定地址与端口

为socket绑定端口号与具体地址。关于IP地址和端口号的知识就不赘述了。但是不能使用已经在用的端口。

netstat -ano|findstr “xxxxx"

检查端口号是否被使用,使用了就会显示使用的程序,未被使用则没有结果。

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

参数1:SOCKET句柄

参数2:sockaddr 结构体指针

struct sockaddr {
        ushort  sa_family;//2字节
        char    sa_data[14];//14字节
};

struct sockaddr_in {
        short   sin_family;//2字节
        u_short sin_port;//2字节
        struct  in_addr sin_addr;//4字节
        char    sin_zero[8];//8字节
};

可以看到两个结构体内容不一样,但是大小是一样的,可以把下面的强转为上面的类型,而并不报错。
可以看到sockaddr_in 更加的方便我们填写端口号和IP地址,因此参数2通常是用sockaddr_in来定义,然后再强转为sockaddr类型。
这里的sin_addr是in_addr类型的结构体,这个结构体的代码如下所示

struct in_addr {
  union {
    struct {
      u_char s_b1;//1字节
      u_char s_b2;//1字节
      u_char s_b3;//1字节
      u_char s_b4;//1字节
    } S_un_b;
    struct {
      u_short s_w1;//2字节
      u_short s_w2;//2字节
    } S_un_w;
    u_long S_addr;//4字节
  } S_un;
};

可以看到,in_addr结构体有三种定义方式:

	struct sockaddr_in si;
	//第一种
	si.sin_addr.S_un.S_un_b.s_b1=192;
	si.sin_addr.S_un.S_un_b.s_b2=168;
	si.sin_addr.S_un.S_un_b.s_b3=0;
	si.sin_addr.S_un.S_un_b.s_b4=1;

	//第二种
	没找到例子

	//第三种
	si.sin_addr.S_un.S_addr=inet_addr("192.168.0.1");

参数3:参数2的长度,sizeof(参数2)

返回值:成功返回0
失败处理方式和创建SOCKET处理方式一样

	if(SOCKET_ERROR==bind(socketServer,(const struct sockaddr *)&si,sizeof(si)))
	{
		int err = WSAGetLastError();//取错误码
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
		return 0;
	}

si在强转的时候是要把变量传入函数bind中,为了防止函数改动该变量,所以加上了const限定词。

5.开始监听

将SOCKET设置为监听状态。

int WSAAPI listen(
  SOCKET s,
  int    backlog
);

函数名中WSAAPI表示调用约定,和系统有关,与程序员无关。其作用有三:
1.函数名字的编译方式;
2.参数的入栈顺序;
3.函数的调用时间。
参数1:SOCKET句柄,该句柄是未连接的状态才能创建成功。
参数2:除了在处理连接,还能挂起多少个连接(处于等待的连接数量),超过(处理中的连接数量+挂起连接数量)则拒绝连接。可以用SOMAXCONN作为默认值,由系统来决定挂起连接数量的最大值。

返回值:成功返回0
失败处理方式和创建SOCKET处理方式一样

	if(SOCKET_ERROR==listen(socketServer,SOMAXCONN))
	{
		int err = WSAGetLastError();//取错误码
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
		return 0;
	}

6.等待客户端连接

注意,accept函数不是接收客户端连接,连接(准确的说:三次握手)是listen那里就搞定了,这里接收的是客户端的SOCKET,因此该函数返回值是一个客户端的SOCKET句柄。如果有多个客户端,需要多次调用该函数。当然同时调用多个就要用到多线程的知识,后面再专门讲。该函数默认是阻塞同步执行的,如果执行到该函数,且没有客户端进行连接,则程序会在这里等待直到有客户端连接。如果有n个客户端要连接服务器,那么就要循环调用该函数n次,如果调用次数大于n则会出现阻塞,无法执行下面的数据交互,因此实作过程中,accept会单独放到某个线程中。

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

参数1:服务器SOCKET句柄,该句柄要先处于监听状态,客户端的连接都由这个服务器SOCKET句柄管理。
参数2:sockaddr类型的结构体的传址调用,获取客户端的地址、端口信息。
参数3:int类型的传址调用,传入参数2的大小。

	struct sockaddr_in clientMsg;
	int clientMsgLen=sizeof(clientMsg);
	SOCKET socketClient = accept(socketServer,(struct sockaddr *)&clientMsg,&clientMsgLen);

注意,参数2/3可以同时设置为NULL,放弃获取客户端地址、端口信息。当然后面还可以用以下函数重新从客户端SOCKET获取信息:

我如何使用视图模型从另一个片段访问函数

带 Hilt 的活动片段通信

最简单的RNN回归模型入门(PyTorch)

netty学习01-IO模型

片段通信问题(尝试调用虚方法)

代码片段 - Golang 实现简单的 Web 服务器