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
低八位00000010高八位00000010=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=低八位00000010高八位00000000
然后再和主版本号进行按位或就得到主版本号在低八位,副版本号在高八位的结果。
当输入版本不存在:
输入 | 描述 | 结果 |
---|---|---|
输入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;
}
名称 | 数值 | 含义 | 解决方案 |
---|---|---|---|
WSASYSNOTREADY | 10091 | 底层网络子系统尚未准备好进行网络通信。 | 重启电脑,并检查库文件是否存在 |
WSAVERNOTSUPPORTED | 10092 | 此特定Windows套接字实现不提供所请求的Windows套接字支持的版本。 | 指定版本不支持,只能换低版本的试试 |
WSAEPROCLIM | 10067 | 已达到对Windows套接字实现支持的任务数量的限制。 | Windows Sockets实现可能限制同时使用它的应用程序的数量 |
WSAEINPROGRESS | 10036 | 正在阻止Windows Sockets 1.1操作。 | 当前函数运行期间,由于某些原因造成阻塞,会返回该错误码,其他操作均禁止 |
WSAEFAULT | 10014 | IpWSAData参数不是有效指针。 | 参数写错了 |
当然还有别的错误码:
/*
* 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_INET | 2 | Internet协议版本4(IPv4)地址系列。 |
AF_INET6 | 23 | Internet协议版本6(IPv6)地址系列。 |
AF_BTH | 32 | 蓝牙地址系列。如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。 |
AF_IRDA | 26 | 红外数据协会(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_STREAM | 1 | 提供带有OOB数据传输机制的顺序,可靠,双向,基于连接的字节流。此套接字类型使用传输控制协议(TCP)作为Internet地址系列(AF_INET或AF_INET6)。 |
SOCK_DGRAM | 2 | 一种支持数据报的套接字类型,它是固定(通常很小)最大长度的无连接,不可靠的缓冲区。此套接字类型使用用户数据报协议(UDP)作为Internet地址系列(AF INET或AF_INET6)。 |
SOCK_RAW | 3 | 提供允许应用程序操作下一个上层协议头的原始套接字。要操作lPv4标头,必须在套接字上设置IP_HDRINCL套接字选项。要操作lPv6标头,必须在套接字上设置IPV6_HDRINCL套接字选项。 |
SOCK_RDM | 4 | 提供可靠的消息数据报。这种类型的一个示例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目。仅在安装了可靠多播协议时才支持此类型值。 |
SOCK_SEQPACKET | 5 | 提供基于数据报的伪流数据包。 |
参数3:协议类型,常见搭配如下表所示:
名称 | 数值 | 协议名称 | 地址参数 | 套接字类型 |
---|---|---|---|---|
IPPROTO_TCP | 6 | 传输控制协议(TCP) | AF_INET或AF_INET6 | SOCK_STREAM |
IPPROTO_UDP | 17 | 用户数据报协议(UDP) | AF_INET或AF INET6 | SOCK_DGRAM |
IPPROTO_ICMP | 1 | Internet控制消息协议(ICMP) | AF UNSPEC,AF_INET或AF_INET6 | SOCK RAW或未指定 |
IPPROTO_IGMP | 2 | Internet组管理协议(IGMP) | AF UNSPEC,AF_INET或AF_INET6 | SOCK RAW或未指定 |
IPPROTO_RM | 113 | 用于可靠多播的PGM协议(Windows Vista以上版本叫IPPROTO_PGM) | AF_INET | SOCK_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获取信息:
我如何使用视图模型从另一个片段访问函数