UDP.01.基础知识+基础模型


https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
本节内容其实属于计算机网络基础方面的知识,大多数在网上以及书本上都有讲这里再次罗列一下,权当记录。

UDP协议头

基础信息

UDP源端口号(2字节)UDP目的端口号(2字节)
校验和(2字节)信息长度(2字节)
1、协议头(包头)总共8字节;
2、UDP源端口号:发送方的端口号;
3、UDP目的端口号:接收方端口号;
4、校验和:发送的数据,通过一定的算法计算出一个值,接收方接到该包,也会用同样的算法,计算出一个校验值,假设,数据包中途被拦截并篡改,那么接收端收到包之后,计算的校验值就与接收方不一样,得知出现问题,看不明白可以类比一下下载文件的md5校验码;
5、信息长度:包头+数据总字节数。

数据报特点

UDP全称是用户数据报协议(User Datagram Protocol),因此它是基于数据报的,其特点是:一份一份的,每份不能断,两份又不能拼倒一起;相对于TCP的数据流:源源不断,一个接着一个,流当然可以随意拆分和合并。
比如:在TCP模型中,客户端连续发送多次消息,服务器在recv的时候,能够一下都读出来。因为是水流嘛,都是源源不断的,所以有多少我都能一下读出来,或者读出来制定的个数,可拼,可断,其实这个也有不好的地方,之前在TCP的几个模型结构里面可以看出来,当我们调试程序的时候,服务器程序下断点停住,这个时候如果客户端发送多个消息,那么服务器继续运行后就会一次性recv出来(除非发送的消息长度大于buff长度);
数据报即使客户端连续发了很多消息,服务器一次recv只能读出一份数据报,后面的消息需要调用相应次数的recv才能读取完毕。

与TCP的对比

1、TCP更复杂,有20字节包头,以及可拓展最大60字节包头;
2、UDP无SYN(连接标志位)FIN(断开连接标志位),说明:UDP不用连接,即客户端无connect函数,服务器无listen,accept函数;
3、UDP无窗口大小,说明不能平衡双方的带宽(TCP窗口大小机制比这个说法更加复杂),一方send几次,另一方就要recv几次,当一方尝试发送超过UDP包限制大小的数据,超过部分会丢失;
4、UDP无ACK(确认收到标记位),说明对方收到无反馈,无法确认对方是否收到;
5、UDP无发送,接收顺序号,说明数据发送,接收无序,收没收到,完不完整无反馈,侧面说明不可靠(一般需要在应用层进行自行验证)。

小结

1、UDP简单,速度更快,更高效,(无连接,无验证,头信息少协议代码判断就少);
2、数据不可靠,(无验证);
3、不管是数据报,还是字节流,都是在传输线路上传输的数据,对于传输线路而言是没有区别的,区别在于数据到了传输层之后的处理方式,数据报就是数据报的形式处理,字节流就以流的特点处理。
思考:如何让其可靠?
我们可以在应用层模仿TCP的验证过程,减小数据报包的大小(包越大,出现丢帧的概率就越大)
还可以为数据包自行添加序号,然后在应用层进行拼接。

UDP基础模型

注意和TCP基础模型的不同(步骤、参数),其实UDP比TCP要简单。

服务器端

无Listen、Accept
1、包含网络头文件网络库
2、打开网络库
3、校验版本
4、创建SOCKET
5、绑定地址与端口
6、与客户端收发消息

1、包含网络头文件网络库

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

64位系统也是这个Ws2_32.lib库,这里不区分大小写

2、打开网络库

打开网络库后这个库里的函数/功能才能使用。https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsastartup

int WSAAPI WSAStartup(
  WORD      wVersionRequested,
  LPWSADATA lpWSAData
);

参数1:网络库的版本,类型是WORD(实际上是unsigned short),长度是2字节
通过MAKEWORD函数将版本号分成两块分别放到2字节里面。

WORD MAKEWORD(
   BYTE bLow,
   BYTE bHigh
);

bLow(低地址):主版本
bHigh(高地址):副版本
参数2:传址调用,lpWSAData:是一个WSADATA结构体指针


typedef struct WSAData {
        WORD                    wVersion;//实际上打开的网络库版本
        WORD                    wHighVersion;//系统可提供的网络库最高版本
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
        unsigned short          iMaxSockets;//返回可用的SOCKET句柄数量,已舍弃
        unsigned short          iMaxUdpDg;//UDP数据包大小,已舍弃
        char FAR *              lpVendorInfo;//供应商信息,已舍弃
} WSADATA, FAR * LPWSADATA;

返回值:
0,表示正确
其他,用WSAGetLastError()获取错误码

int main()
{
	WORD wVersionRequested = MAKEWORD(2,2);//版本
	WSADATA wsaDATA;
	int iret = WSAStartup(wVersionRequested,&wsaDATA);
	if (iret!=0)
	{
		//有错
		switch(iret)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启。。。");
				break; 
			case WSAVERNOTSUPPORTED: 
				printf("解决方案:更新网络库");
				break; 
			case WSAEINPROGRESS: 
				printf("解决方案:重启。。。");
				break; 
			case WSAEPROCLIM: 
				printf("解决方案:网络连接达到上限或阻塞,关闭不必要软件");
				break; 
			case WSAEFAULT:
				printf("解决方案:程序有误");
				break;
		}
		getchar();
		return 0;
	}
	
	//关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

打开后要记得关闭网络库:

WSACleanup();

3、校验版本

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

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

版本不对记得关闭网络库并返回

4、创建SOCKET

https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket

SOCKET WSAAPI socket(
  int af,
  int type,
  int protocol
);

参数1:地址类型

名称取值含义
AF_INET2Internet协议版本4(IPv4)地址系列。
AF_INET623Internet协议版本6(IPv6)地址系列。
AF_BTH32蓝牙地址系列。如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。
AF_IRDA26红外数据协会(IrDA)地址系列。仅当计算机安装了红外端口和驱动程序时,才支持此地址系列。

参数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(仅在安装了可靠多播协议时才支持此协议值)

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

// 4、创建SOCKET
	SOCKET socketServer = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
	if(INVALID_SOCKET == socketServer)
	{
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	
	

	closesocket(socketServer);//与4、创建SOCKET对应,如果有创建客户端SOCKET句柄,也要关闭

5、绑定地址与端口

关于服务器为什么要使用bind而客户端不用,可以看这里https://blog.csdn.net/qq_29344757/article/details/71616748
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind

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格式)
参数3:参数2的长度,写sizeof(参数2)
返回值:成功返回0
失败处理方式和创建SOCKET处理方式一样

6、与客户端收发消息

收recvfrom

从协议缓冲区(UDP协议规定的)将数据复制到我们的buff(参数2),该函数是阻塞的。

int WSAAPI recvfrom(
  SOCKET   s,
  char     *buf,
  int      len,
  int      flags,
  sockaddr *from,
  int      *fromlen
);

参数1:服务器端的SOCKET句柄,这里和TCP的recv不一样,recv的参数1是客户端SOCKET句柄,recv的阻塞是等这里指定的客户端,是1对1的关系;UDP则没有指定接收哪个客户端的消息,因此recvfrom中服务器SOCKET和客户端SOCKET是1对多的关系。
参数2:字符数组,用于客户端消息的存储。关于字符数组的大小设置原则应尽量设置大一些,避免不必要的包的拆分和组合。

网络类型MTUTCP最大长度UDP最大长度
局域网以太网1500 min ⁡ ( 1500 − ( 20 ∼ 60 ) T C P 包 头 − 2 0 I P 包 头 ) = 1420 \min(1500-(20\sim60)_{TCP包头}-20_{IP包头})=1420 min(1500(2060)TCP20IP)=1420 1500 − 8 U D P 包 头 − 2 0 I P 包 头 = 1472 1500-8_{UDP包头}-20_{IP包头}=1472 15008UDP20IP=1472
广域网路由器576 min ⁡ ( 576 − ( 20 ∼ 60 ) T C P 包 头 − 2 0 I P 包 头 ) = 496 \min(576-(20\sim60)_{TCP包头}-20_{IP包头})=496 min(576(2060)TCP20IP)=496 576 − 8 U D P 包 头 − 2 0 I P 包 头 = 548 576-8_{UDP包头}-20_{IP包头}=548 5768UDP20IP=548

两种网络可设置的大小不一样,对于TCP而言,设置为1420还是496都可以,反正可靠流传输,不会丢;对于UDP,按小的弄:548。

参数3:设定要读取的数据报字节个数,一般设置为参数2的大小
如果设置的数据报字节个数比参数2小,那么后面多出来的那部分估计会被丢弃;如果是TCP则可下一次再读取出来。
参数4:数据的读取方式,这个之前有:
默认是0即可。正常情况下recvfrom根据参数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。由于不清空被读取的数据,缓冲区还是不变:
ab

如果再次执行recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据还是得到a,b。
参数5:sockaddr结构体的传址调用,获取到发来消息的客户端信息
参数6:客户端信息的大小
返回值:
成功:返回读出来的字节大小,如果没有信息可读则在这里阻塞。
这里和TCP不一样的是,TCP如果客户端下线会收到0(正常下线)和10054(点×强制下线)。
失败:返回SOCKET_ERROR。

struct sockaddr sa;
		int iSaLen = sizeof(sa);
		char strRecvBuff[548]={0};

		if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("服务器recvfrom失败错误码为:%d\n",err);
		}
发sendto

该函数将数据复制到系统的协议发送缓冲区,在伺机发送出去。

int WSAAPI sendto(
  SOCKET         s,
  const char     *buf,
  int            len,
  int            flags,
  const sockaddr *to,
  int            tolen
);

参数1:自己的SOCKET句柄(这里和send不一样,send的参数1是目标SOCKET句柄)
参数2:要发送的字符串
参数3:字符串长度
参数4:数据的发送方式。默认是0即可。当然还有其他取值,意义如下表:

数值含义
0(默认值)从数据缓冲区发送数据后清空被发送的数据
MSG_OOB传输带外数据,每次可以额外传输1个字节的数据,具体数据内容可以自己定义,这个方法可以用分别调用两次send函数,而且在不同TCP协议规范这个模式还不怎么兼容,不推荐使用
MSG_DONTROUTE指定数据不应受路由限制。由于Windows套接字服务提供程序可以选择忽略此标志,因此该模式只能在Linux系统下面用。

参数5:目标IP地址端口号的sockaddr结构体
参数6:sockaddr结构体大小
返回值:
成功:返回发送的字节大小。
失败:返回SOCKET_ERROR。

//发
		if(sendto(socketServer,"This is a message from server~!",sizeof("This is a message from server~!"),0,&sa,sizeof(sa))==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("服务器sendto失败错误码为:%d\n",err);
		}

客户端

无Connect
1、包含网络头文件网络库
2、打开网络库
3、校验版本
4、创建SOCKET(这里变量名改下)

// 4、创建SOCKET
	SOCKET socketClient = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
	if(INVALID_SOCKET == socketClient)
	{
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}
//服务器地址与端口
	struct sockaddr_in si;
	si.sin_family = AF_INET;//这里要和创建SOCKET句柄的参数1类型一样
	si.sin_port = htons(9527);//用htons宏将整型转为端口号的无符号整型
	si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");

5、与服务器端收发消息
以上步骤和服务器端步骤基本一致,少了绑定,在创建SOCKET步骤上是创建服务器的地址端口结构体。

5、与服务器端收发消息

这里为了和服务器互动,不阻塞卡死,先发再收。

//发
		char strSendBuff[548] = {0};
		scanf("%s",strSendBuff);
		if(sendto(socketClient,strSendBuff,sizeof(strSendBuff),0,(const struct sockaddr *)&si,sizeof(si))==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("客户端sendto失败错误码为:%d\n",err);
			continue;
		}

		//收
		struct sockaddr sa;
		int iSaLen = sizeof(sa);
		char strRecvBuff[548]={0};

		if(recvfrom(socketClient,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("客户端recvfrom失败错误码为:%d\n",err);
			continue;
		}
		printf("客户端recvfrom消息是:%s\n",strRecvBuff);

优化: 处理点×关闭

点×关闭属于强制关闭,这个时候程序还有很多内核对象(SOCKET句柄)没有销毁,很容易造成内存泄露。
首先把SOCKET句柄改成全局变量,然后写一段在强制关闭事件发生后要处理的代码

SOCKET socketServer;

//处理强制关闭事件
BOOL WINAPI CtrlFun(DWORD dwType)
{
	switch (dwType)
	{
	case CTRL_CLOSE_EVENT:
		//关闭socket
		closesocket(socketServer);
		//关闭网络库
		WSACleanup();
		break;
	}
	return FALSE;
}

在主函数开头加一句:

//投递关闭事件
	SetConsoleCtrlHandler(CtrlFun, TRUE);

这里说明一下:
发送消息的代码使用scanf来接收客户的输入,如果客户输入字符串中带有空格,空格会作为字符串的截断符号,将字符串分成多个数据报进行发送,如果要使得忽略空格作为截断符号,这里可以将scanf改成:

gets(strbuf);

完整代码

服务器

#include <stdio.h>
//1、包含网络头文件网络库
# include <WinSock2.h>
# pragma comment(lib, "Ws2_32.lib")

SOCKET socketServer;

//处理强制关闭事件
BOOL WINAPI CtrlFun(DWORD dwType)
{
	switch (dwType)
	{
	case CTRL_CLOSE_EVENT:
		//关闭socket
		closesocket(socketServer);
		//关闭网络库
		WSACleanup();
		break;
	}
	return FALSE;
}

int main()
{
	//投递关闭事件
	SetConsoleCtrlHandler(CtrlFun, TRUE);

	WORD wVersionRequested = MAKEWORD(2,2);//版本
	WSADATA wsaDATA;

	//2、打开网络库
	int iret = WSAStartup(wVersionRequested,&wsaDATA);
	if (iret!=0)
	{
		//有错
		switch(iret)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启。。。");
				break; 
			case WSAVERNOTSUPPORTED: 
				printf("解决方案:更新网络库");
				break; 
			case WSAEINPROGRESS: 
				printf("解决方案:重启。。。");
				break; 
			case WSAEPROCLIM: 
				printf("解决方案:网络连接达到上限或阻塞,关闭不必要软件");
				break; 
			case WSAEFAULT:
				printf("解决方案:程序有误");
				break;
		}
		getchar();
		return 0;
	}

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

	// 4、创建SOCKET
	socketServer = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
	if(INVALID_SOCKET == socketServer)
	{
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	//5、绑定地址与端口
	struct sockaddr_in si;
	si.sin_family = AF_INET;//这里要和创建SOCKET句柄的参数1类型一样
	si.sin_port = htons(9527);//用htons宏将整型转为端口号的无符号整型
	si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");

	if(bind(socketServer,(struct sockaddr*)&si,sizeof(si))==SOCKET_ERROR)
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
	}

	//7.与客户端收发消息
	while(1)
	{
		//收
		struct sockaddr sa;
		int iSaLen = sizeof(sa);
		char strRecvBuff[548]={0};

		if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("服务器recvfrom失败错误码为:%d\n",err);
			continue;
		}
		printf("服务器recvfrom消息是:%s\n",strRecvBuff);

		//发
		if(sendto(socketServer,"This is a message from server~!",sizeof("This is a message from server~!"),0,&sa,sizeof(sa))==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("服务器sendto失败错误码为:%d\n",err);
			continue;
		}

	}
	

	closesocket(socketServer);//与4、创建SOCKET对应,如果有创建客户端SOCKET句柄,也要关闭
	//关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

客户端

#include <stdio.h>
//1、包含网络头文件网络库
# include <WinSock2.h>
# pragma comment(lib, "Ws2_32.lib")


SOCKET socketClient;

//处理强制关闭事件
BOOL WINAPI CtrlFun(DWORD dwType)
{
	switch (dwType)
	{
	case CTRL_CLOSE_EVENT:
		//关闭socket
		closesocket(socketClient);
		//关闭网络库
		WSACleanup();
		break;
	}
	return FALSE;
}

int main()
{
	//投递关闭事件
	SetConsoleCtrlHandler(CtrlFun, TRUE);

	WORD wVersionRequested = MAKEWORD(2,2);//版本
	WSADATA wsaDATA;

	//2、打开网络库
	int iret = WSAStartup(wVersionRequested,&wsaDATA);
	if (iret!=0)
	{
		//有错
		switch(iret)
		{
			case WSASYSNOTREADY: 
				printf("解决方案:重启。。。");
				break; 
			case WSAVERNOTSUPPORTED: 
				printf("解决方案:更新网络库");
				break; 
			case WSAEINPROGRESS: 
				printf("解决方案:重启。。。");
				break; 
			case WSAEPROCLIM: 
				printf("解决方案:网络连接达到上限或阻塞,关闭不必要软件");
				break; 
			case WSAEFAULT:
				printf("解决方案:程序有误");
				break;
		}
		getchar();
		return 0;
	}

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

	// 4、创建SOCKET
	socketClient = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
	if(INVALID_SOCKET == socketClient)
	{
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}

	//服务器地址与端口
	struct sockaddr_in si;
	si.sin_family = AF_INET;//这里要和创建SOCKET句柄的参数1类型一样
	si.sin_port = htons(9527);//用htons宏将整型转为端口号的无符号整型
	si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");

	//客户端不用绑定操作
	/*
	if(bind(socketServer,(struct sockaddr*)&si,sizeof(si))==SOCKET_ERROR)
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\n",err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
	}
	*/

	//5、与客户端收发消息
	while(1)
	{
		//发
		char strSendBuff[548] = {0};
		scanf("%s",strSendBuff);
		if(sendto(socketClient,strSendBuff,sizeof(strSendBuff),0,(const struct sockaddr *)&si,sizeof(si))==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("客户端sendto失败错误码为:%d\n",err);
			continue;
		}

		//收
		struct sockaddr sa;
		int iSaLen = sizeof(sa);
		char strRecvBuff[548]={0};

		if(recvfrom(socketClient,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
		{
			int err = WSAGetLastError();//取错误码
			printf("客户端recvfrom失败错误码为:%d\n",err);
			continue;
		}
		printf("客户端recvfrom消息是:%s\n",strRecvBuff);	

	}
	

	closesocket(socketClient);//与4、创建SOCKET对应,如果有创建客户端SOCKET句柄,也要关闭
	//关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

oldmao_2000

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

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

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

打赏作者

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

抵扣说明:

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

余额充值