TCP.01.基础通信模型


试看: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/
在这里插入图片描述

基本概念

网络编程:从理论上看是基于网络协议的编程;从代码角度上看,就是就是调用对应的函数,传递对应的参数。
网络协议:协议即规则,网络协议就是双方通信的规则。
协议就好比语言,要想和中国人打交道就要讲中国话,和美国人讲话就要讲英语,这个就好比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 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:实际上打开的网络库版本是多少
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;
	}
名称数值含义解决方案
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"

检查端口号是否被使用,使用了就会显示使用的程序,未被使用则没有结果。
在设置端口号的时候需要用到函数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读取数据缓冲区指定长度的数据后(指定长度大于数据长度则全部读取),数据缓冲区中被读取的数据会清除,把空间留出来给别的消息进来(不清理的话时间长了内存会溢出,数据缓冲区数据结构相当于队列)。
例如数据缓冲区中有如下数据:

abcdef

调用recv(socketClient,buff,2,0);从数据缓冲区读取两个字节的数据得到a,b。则变成

cdef

这个时候再调用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。由于不清空被读取的数据,缓冲区还是不变:

abcdef

如果再次执行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检查中将圈内的选择为否
在这里插入图片描述

使用断点调试程序,查看在没有客户端连接服务器的情况下,服务器会在哪个步骤等待?

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oldmao_2000

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值