文章目录
试看:https://www.bilibili.com/video/BV1cb411w7sZ?p=1
购买:https://study.163.com/course/introduction.htm?courseId=1006358018
先试看一下,做点笔记,当然还会结合之前教过的网络编程知识进行补充。
作者是C3程序猿
以下简介搬运之原课程介绍
课程章节:
第一章 TCP基础模型的讲解以及代码实现
第二章 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/
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210701112744561.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29sZG1hb18yMDAx,size_16,color_FFFFFF,t_70)
基本概念
网络编程:从理论上看是基于网络协议的编程;从代码角度上看,就是就是调用对应的函数,传递对应的参数。
网络协议:协议即规则,网络协议就是双方通信的规则。
协议就好比语言,要想和中国人打交道就要讲中国话,和美国人讲话就要讲英语,这个就好比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(Windows异步套接字),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:实际上打开的网络库版本是多少
wHighVersion:系统能提供给我们最高的版本
iMaxSockets:返回可用的socket的数量,2.0版本之后就没用了
iMaxUdpDg:UDP数据报信息的大小,2.0版本之后就没用了
lpVendorlnfo:供应商特定的信息,2.0版本之后就没用了
zDescription:当前库的描述信息,2.0是第二版的意思
szSystemstatus :Running表示正在运行
否则返回错误码,可以写一段代码判断是否执行成功。具体内容可以看MSDN。
#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"
检查端口号是否被使用,使用了就会显示使用的程序,未被使用则没有结果。
在设置端口号的时候需要用到函数htons
关于这个函数的说明可以参考:https://blog.csdn.net/myyllove/article/details/83380209
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类型。(搞这么复杂的原因还是考虑兼容性,很多系统里面只支持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会单独放到某个线程中。
参考:
https://www.sohu.com/a/490433296_121124377
https://blog.csdn.net/dearQiHao/article/details/102844366
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获取信息:
int WSAAPI getpeername(
SOCKET s,
sockaddr *name,
int *namelen
);
例如:
struct sockaddr_in clientMsg;
int clientMsgLen=sizeof(clientMsg);
SOCKET socketClient = accept(socketServer,NULL,NULL);
getpeername(socketClient,(struct sockaddr *)&clientMsg,&clientMsgLen);
如果要得到本机的地址、端口信息,可以用:
int WSAAPI getsockname(
SOCKET s,
sockaddr *name,
int *namelen
);
不举例了,和上面一样,不过getsockname里面的SOCKET句柄无论是什么都是得到本机的信息。
返回值:
成功返回客户端句柄。
失败返回INVALID_SOCKET(注意和前面listen返回错误值名称不一样)
if (INVALID_SOCKET == socketClient)
{
int err = WSAGetLastError();//取错误码
closesocket(socketServer);//释放
WSACleanup();//清理网络库
return 0;
}
7.与客户端收发消息
收发参数都差不多,详细讲一个即可。recv原理是:客户端有消息发来,服务器会自动接收并放入消息缓冲区中,调用recv后会从消息缓冲区中读取客户端(参数1)消息并复制到buff(参数2)中。
收
int WSAAPI recv(
SOCKET s,
char *buf,
int len,
int flags
);
参数1:客户端句柄,有多个客户端句柄的时候,根据句柄来接收消息。
参数2:客户端消息的存储空间(字符数组),一般为1500字节,这个大小和最大传输单元(MTU)限制有关,MTU是链路层中的网络对数据帧的一个限制,以以太网为例,MTU是1500个字节。
参数3:想要读取的字节个数,一般是参数2的字节数-1,把\0字符串结尾留出来。
参数4:数据的读取方式。默认是0即可。正常情况下recv根据参数3读取数据缓冲区指定长度的数据后(指定长度大于数据长度则全部读取),数据缓冲区中被读取的数据会清除,把空间留出来给别的消息进来(不清理的话时间长了内存会溢出,数据缓冲区数据结构相当于队列)。
例如数据缓冲区中有如下数据:
a | b | c | d | e | f |
---|
调用recv(socketClient,buff,2,0);从数据缓冲区读取两个字节的数据得到a,b。则变成
c | d | e | f |
---|
这个时候再调用recv(socketClient,buff,2,0);从数据缓冲区读取两个字节的数据得到c,d。
懂得正常逻辑后我们可以看下其他几种模式。
数值 | 含义 |
---|---|
0(默认值) | 从数据缓冲区读取数据后清空被读取的数据 |
MSG_PEEK(不建议使用,内存会爆) | 从数据缓冲区读取数据后不清空被读取的数据 |
MSG_OOB | 接收带外数据,每次可以额外传输1个字节的数据,具体数据内容可以自己定义,这个方法可以用分别调用两次send函数,而且在不同TCP协议规范这个模式还不怎么兼容,因此也不推荐使用 |
MSG_WAITALL | 等待知道系统缓冲区字节数大于等于参数3所指定的字节数,才开始读取 |
如果使用MSG_PEEK模式,那么调用recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据得到a,b。由于不清空被读取的数据,缓冲区还是不变:
a | b | c | d | e | f |
---|
如果再次执行recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据还是得到a,b。
返回值:
返回读取的字节大小;
如果客户端连接后没有发送数据,则阻塞等待;
如果客户端连接后没有发送数据然后关闭SOCKET句柄,那么返回0;
执行失败返回SOCKET_ERROR,这里不需要关闭服务器SOCKET句柄。
代码实例:
char buff[1500]={0};
int res = recv(socketClient,buff,sizeof(buff),0);
if (res==0)
{
printf("连接中断或客户端已断开连接!");
}
else if (res == SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
//根据错误码进行相应处理。思考这里是否需要关闭SOCKET
}
else
{
printf("收到长度为%d的信息,内容为:%s\n", res, buff);//打印正确接收到的数据
}
发
int WSAAPI send(
SOCKET s,
const char *buf,
int len,
int flags
);
该函数将要发送的数据(参数2:buf)复制到系统缓冲区,由计算机等待空闲时间发出去,这里发数据包涉及到CSMA/CD机制,不展开。
参数1,目标SOCKET句柄。
参数2,要发送的字节串,长度一般小于分片长度1500。因为发送时候,协议要进行包装,加上协议信息,也叫协议头,或者叫包头。具体格式可以百度,包头大小根据协议不同大小也不一样,数据包结尾还要有结束标志或状态确认标志,因此通常发送字节串长度1400就差不多接近上限了。当然如果非要发超过1500字节的数据,那么系统会自动进行分片处理,接收方还要组包,会一定程度上降低效率。
参数3,要发送的字节个数,如果这个数字比参数2的字节串长度要小,那么字节串要被截断。
参数4,:数据的发送方式。默认是0即可。当然还有其他取值,意义如下表:
数值 | 含义 |
---|---|
0(默认值) | 从数据缓冲区发送数据后清空被发送的数据 |
MSG_OOB | 传输带外数据,每次可以额外传输1个字节的数据,具体数据内容可以自己定义,这个方法可以用分别调用两次send函数,而且在不同TCP协议规范这个模式还不怎么兼容,不推荐使用 |
MSG_DONTROUTE | 指定数据不应受路由限制。由于Windows套接字服务提供程序可以选择忽略此标志,因此该模式只能在Linux系统下面用。 |
返回值:
成功则返回写入的字节数。
执行失败则返回SOCKET_ERROR,可以用WSAGetLastError();获取错误码,同样这里不需要关闭服务器SOCKET句柄。
代码实例:
if (send(socketClient,"aaa\0bbb",sizeof("aaa\0bbb"),0)== SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
//不用关闭socket
//根据实际情况处理
}
注意:在send函数中设置发送消息的长度最好是用sizeof来取,如果是用strlen来取,上面的例子中在字符串"aaa\0bbb"中有结束标记\0,因此strlen只会求的长度为3。
例如:
printf("%d",sizeof("aaa\0bbb"));
printf("%d",strlen("aaa\0bbb"));
上面结果是8,下面结果是3。
客户端
同样要有头文件和库文件,然后大概步骤是:
打开网络库
校验版本
创建SOCKET
链接到服务器
与服务器收发消息
前面三个步骤和服务器的代码是一样的套路
最后一个步骤和服务器代码基本一样,只是先发再收
因此我们着重讲下客户端的这个步骤:链接到服务器
先把服务器几个相同的步骤代码copy一下:
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
int main(void)
{
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
WORD wdVersion=MAKEWORD(2,2);
int a=*((char*)&wdVersion);
int b=*((char*)&wdVersion+1);
LPWSADATA lpw = (WSADATA*)malloc(sizeof(WSADATA));
int nRes = WSAStartup(wdVersion,lpw);
if (0 != nRes)
{
switch(nRes)
{
case WSASYSNOTREADY:
printf("解决方案:重启。。。");
break;
case WSAVERNOTSUPPORTED:
break;
case WSAEINPROGRESS:
break;
case WSAEPROCLIM:
break;
case WSAEFAULT:
break;
}
return 0;
}
//校验版本
if (2!=HIBYTE(lpw->wVersion)|| 2!=LOBYTE(lpw->wVersion))
{
printf("版本没问题!");
WSACleanup();
return 0;
}
//这里创建的是服务器的SOCKET句柄
SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(INVALID_SOCKET == socketServer)
{
int err=WSAGetLastError();
//清理网络库,不关闭句柄
WSACleanup();
return 0;
}
//这里写连接服务器的代码
closesocket(socketServer);
WSACleanup();
free(lpw);
system("pause");
return 0;
}
链接到服务器
函数形式为:
int WSAAPI connect(
SOCKET s,
const sockaddr *name,
int namelen
);
参数1:服务器SOCKET句柄
参数2:服务器IP地址以及端口号信息
参数3:参数2结构体的大小
返回值:
成功:返回0
失败:返回SOCKET_ERORR
//这里写连接服务器的代码
struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;
serverMsg.sin_port = htons(1245);
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if(connect(socketServer,(struct sockaddr*)&serverMsg,sizeof(serverMsg)) == SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
closesocket(socketServer);//释放
WSACleanup();//清理网络库
return 0;
}
然后再把发送接收数据代码copy过来。注意:客户端是先发再收。
两边的端口要设置一样的,否则connect会报10061错误
if (send(socketServer,"Hi,I'm Client!",sizeof("Hi,I'm Client!"),0)== SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("发送失败错误码为:%d\n",err);
//不用关闭socket
//根据实际情况处理
}
printf("发送消息成功!\n");
char buff[1500]={0};
int res = recv(socketServer,buff,sizeof(buff),0);
if (res==0)
{
printf("连接中断或客户端已断开连接!");
}
else if (res == SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
}
else
{
printf("%d %s\n",res,buff);//打印正确接收到的数据
}
总结
整个代码步骤比较清晰的,里面由于采用的是默认阻塞方式进行编程的,因此各个步骤的先后顺序不能调换,例如服务器和客户机连接成功后,服务器是先收再发,那么客户机就是先发再收。
上面的代码只能发送和接收一次,而且发送的消息是写死了,我们先可以用scanf来修改一下,自定义发送的消息。然后加上循环,可以发送多次消息。
服务器端代码
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
int main(void)
{
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
WORD wdVersion=MAKEWORD(2,2);
int a=*((char*)&wdVersion);
int b=*((char*)&wdVersion+1);
LPWSADATA lpw = (WSADATA*)malloc(sizeof(WSADATA));
int nRes = WSAStartup(wdVersion,lpw);
if (0 != nRes)
{
switch(nRes)
{
case WSASYSNOTREADY:
printf("解决方案:重启。。。");
break;
case WSAVERNOTSUPPORTED:
break;
case WSAEINPROGRESS:
break;
case WSAEPROCLIM:
break;
case WSAEFAULT:
break;
}
return 0;
}
//校验版本
if (2!=HIBYTE(lpw->wVersion)|| 2!=LOBYTE(lpw->wVersion))
{
printf("版本有问题!");
WSACleanup();
return 0;
}
SOCKET socketServer=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(INVALID_SOCKET == socketServer)
{
int err=WSAGetLastError();
//清理网络库,不关闭句柄
WSACleanup();
return 0;
}
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);//用htons宏将整型转为端口号的无符号整型
si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
if(SOCKET_ERROR==bind(socketServer,(const struct sockaddr *)&si,sizeof(si)))
{
int err = WSAGetLastError();//取错误码
printf("服务器bind失败错误码为:%d\n",err);
closesocket(socketServer);//释放
WSACleanup();//清理网络库
return 0;
}
printf("服务器端bind成功!\n");
if(SOCKET_ERROR==listen(socketServer,SOMAXCONN))
{
int err = WSAGetLastError();//取错误码
printf("服务器监听失败错误码为:%d\n",err);
closesocket(socketServer);//释放
WSACleanup();//清理网络库
return 0;
}
printf("服务器端监听成功!\n");
struct sockaddr_in clientMsg;
int clientMsgLen=sizeof(clientMsg);
SOCKET socketClient = accept(socketServer,(struct sockaddr *)&clientMsg,&clientMsgLen);
if (INVALID_SOCKET == socketClient)
{
int err = WSAGetLastError();//取错误码
printf("获取客户端句柄失败错误码为:%d\n",err);
closesocket(socketServer);//释放
WSACleanup();//清理网络库
return 0;
}
printf("客户端连接成功!\n");
char recvbuff[1500]={0};
char csendbuff[1500]={0};
while(1)
{
//这里最后用sizeof(buff),如果指定长度短会有空字符漏到下一次循环
int res = recv(socketClient,recvbuff,sizeof(recvbuff),0);
if (res==0)
{
printf("连接中断或客户端已断开连接!");
}
else if (res == SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("server recv失败错误码为:%d\n",err);
}
else
{
printf("收到客户机消息长度:%d,具体内容为:%s\n",res,recvbuff);//打印正确接收到的数据
}
scanf("%s",csendbuff);
if (send(socketClient,csendbuff,sizeof(csendbuff),0)== SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("server send失败错误码为:%d\n",err);
//不用关闭socket
//根据实际情况处理
}
}
closesocket(socketClient);
closesocket(socketServer);
WSACleanup();
free(lpw);
system("pause");
return 0;
}
客户端代码
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
int main(void)
{
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
WORD wdVersion=MAKEWORD(2,2);
int a=*((char*)&wdVersion);
int b=*((char*)&wdVersion+1);
LPWSADATA lpw = (WSADATA*)malloc(sizeof(WSADATA));
int nRes = WSAStartup(wdVersion,lpw);
if (0 != nRes)
{
switch(nRes)
{
case WSASYSNOTREADY:
printf("解决方案:重启。。。");
break;
case WSAVERNOTSUPPORTED:
break;
case WSAEINPROGRESS:
break;
case WSAEPROCLIM:
break;
case WSAEFAULT:
break;
}
return 0;
}
//校验版本
if (2!=HIBYTE(lpw->wVersion)|| 2!=LOBYTE(lpw->wVersion))
{
printf("版本有问题!");
WSACleanup();
return 0;
}
//这里创建的是服务器的SOCKET句柄
SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(INVALID_SOCKET == socketServer)
{
int err=WSAGetLastError();
//清理网络库,不关闭句柄
WSACleanup();
return 0;
}
printf("客户端句柄创建成功!\n");
//这里写连接服务器的代码
struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;
serverMsg.sin_port = htons(12345);
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if(connect(socketServer,(struct sockaddr*)&serverMsg,sizeof(serverMsg)) == SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("连接服务器错误码为:%d\n",err);
closesocket(socketServer);//释放
WSACleanup();//清理网络库
return 0;
}
printf("服务器端连接成功!\n");
char ssendbuff[1500]={0};
char recvbuff[1500]={0};
while(1)
{
scanf("%s",ssendbuff);
if (send(socketServer,ssendbuff,sizeof(ssendbuff),0)== SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("client send失败错误码为:%d\n",err);
//不用关闭socket
//根据实际情况处理
}
//printf("发送消息成功!\n");
int res = recv(socketServer,recvbuff,sizeof(recvbuff),0);
if (res==0)
{
printf("连接中断或客户端已断开连接!");
}
else if (res == SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("client recv失败错误码为:%d\n",err);
}
else
{
printf("收到服务器消息长度:%d,具体内容为:%s\n",res,recvbuff);//打印正确接收到的数据
}
}
closesocket(socketServer);
WSACleanup();
free(lpw);
system("pause");
return 0;
}
小结
但是如果多个客户端同时连接一个服务器,服务器如果和其中一个客户端进行通讯的过程中,该客户端没有发送消息,那么服务器端就可以在recv这个函数阻塞,无法和其他客户端进行通讯,这个时候要进行改进可以使用select模式,让那些有请求的客户端和服务器进行通讯。
以上代码是vc6.0的,升级到2019的时候会报错:
在绑定地址的过程中出现了错误:
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
这里提示要使用新版的函数,因为上面的老版函数只支持IPv4版本,当然也可以设置编译器进行屏蔽:
# include <ws2tcpip.h>//用新函数要带这个头文件
inet_pton(AF_INET,"127.0.0.1", &si.sin_addr.S_un.S_addr);
IPv6用AF_INET6
使用老函数的方法是在项目属性页去掉SDL检查中将圈内的选择为否
使用断点调试程序,查看在没有客户端连接服务器的情况下,服务器会在哪个步骤等待?