Windows下的socket编程

前言

经过一周的时间,我又回来啦,这周我主要学习的是Windows下的socket网络编程。本篇博客的内容包括socket的简介、TCP/IP协议的讲解、TCP socket编程实例、UDP socket编程实例以及相关类的封装,涉及到的知识都比较简单,希望阅读完本篇博客后会对你有所帮助。

socket编程简介

socket编程是网络常用的编程,我们通过在网络中创建socket关键字来实现网络间的通信。那么socket到底是什么呢?百度百科给了以下解释:

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口

简单来说,套接字是人们抽象出来的一个概念,它其实就是应用程序通过网络协议来进行通讯的接口。

socket的主要类型

  1. 流套接字(SOCK_STREAM)

流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议
我们进行TCP客户端和服务端的搭建时,就会使用到流套接字。

  1. 数据报套接字(SOCK_DGRAM)

数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP (User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
我们进行UDP客户端和服务端的搭建时,就会使用到数据报套接字。

  1. 原始套接字(SOCK_RAW)

原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接

TCP/IP三次握手建立链接

当你想要理解socket你就必须先理解tcp/ip,它们之间好比送信的线路和驿站的作用。
TCP/IP协议不同于iso的7个分层,它是根据这7个分层,将其重新划分,归类到四个抽象层中:

应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
传输层:TCP,UDP
网络层:IP,ICMP,OSPF,EIGRP,IGMP
数据链路层:SLIP,CSLIP,PPP,MTU

每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的:
Windows下的socket编程_第1张图片
那么究竟TCP/IP协议是怎么通讯建立链接的呢?这就是我要讲的三次握手。
TCP协议通过三个报文段完成连接的建立,这个过程称为三次握手,过程如下图所示。
  Windows下的socket编程_第2张图片
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进如SYN—RECV状态。
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
一个完整的三次握手也就是: 请求—应答—再次确认。

TCP socket编程实例

通过对上面TCP三次握手建立链接的认识能够让我们更好的理解TCP socket编程。下图展示了TCP socket编程的步骤:
Windows下的socket编程_第3张图片

服务器端:

其过程是首先服务器方要先启动,并根据请求提供相应服务

  1. 打开一通信通道并告知本地主机,它愿意在某一公认地址上的某端口接收客户请求;
  2. 等待客户请求到达该端口;
  3. 接收到客户端的服务请求时,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
  4. 返回第(2)步,等待另一客户请求。
  5. 关闭服务器

客户端:

  1. 打开一通信通道,并连接到服务器所在主机的特定端口;
  2. 向服务器发服务请求报文,等待并接收应答;继续提出请求…
  3. 请求结束后关闭通信通道并终止。

下面直接进入实例:

客户端代码:

#include 
#include 
#pragma comment (lib,"ws2_32.lib")
using namespace std;
int main()
{
	char sendBuf[1024];
	char receiveBuf[1024];
	while (1)
	{
		WSADATA wsadata;
		if (0 == WSAStartup(MAKEWORD(2, 2), &wsadata))
		{
			cout << "客户端嵌套字已打开" << endl;
		}
		else
		{
			cout << "客户端嵌套字打开失败" << endl;
		}
		SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);

		SOCKADDR_IN client_in;
		client_in.sin_addr.S_un.S_addr = inet_addr("172.21.32.239");//将网络地址字符串转换成二进制形式
		client_in.sin_family = AF_INET;
		client_in.sin_port = htons(6000);

		connect(clientSocket, (SOCKADDR*)&client_in, sizeof(SOCKADDR));
		recv(clientSocket, receiveBuf, 1024, 0);
		cout << "收到:" << receiveBuf << endl;
		/*printf_s("%s\n", receiveBuf);*/
		cout << "发出:";
		gets_s(sendBuf, 1024);
		send(clientSocket, sendBuf, 1024, 0);
		closesocket(clientSocket);
		WSACleanup();
	}
	return 0;
}

服务端代码:

#include 
#include 
#include 
#define _WINSOCK_DEPRECATED_NOWARNINGS
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main()
{
	char sendBuf[1024];
	char receiveBuf[1024];
	while (1)
	{
		//创建套接字,socket前的一些检查工作.
	//服务的启动
		WSADATA wsadata;//wsa 即windows socket async 异步套接字
		if (0 != WSAStartup(MAKEWORD(2, 2), &wsadata))
		{
			cout << "套接字未打开" << endl;
			return 0;
		}
		else
		{
			cout << "已打开套接字" << endl;
		}
		SOCKET serSocket = socket(AF_INET, SOCK_STREAM, 0);//创建可识别的套接字//parm1: af 地址协议族 ipv4 ipv6
		                                                   //parm2:type 传输协议类型 流式套接字(SOCK_STREAM),数据包套接字(SOCK_DGRAM)
		                                                   //parm3:ptotoc1 使用具体的某个传输协议
		
		SOCKADDR_IN addr;                                  //需要绑定的参数,主要是本地的socket的一些信息。
		addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);     //ip地址,htonl即host本机 to:to  n:net l:unsigned long 大端存储,低字节在高位
		addr.sin_family = AF_INET;
		addr.sin_port = htons(6000);                       //端口 htons将无符号短整型转化为网络字节序

		bind(serSocket, (SOCKADDR*)&addr, sizeof(SOCKADDR));//绑定完成
		listen(serSocket, 5);                               //监听窗口
		SOCKADDR_IN clientsocket;
		int len = sizeof(SOCKADDR);
		SOCKET serConn = accept(serSocket, (SOCKADDR*)&clientsocket, &len);//于客户端建立链接
		
		cout << "发出:";
		gets_s(sendBuf, 1024);
		send(serConn, sendBuf, 1024, 0);

		recv(serConn, receiveBuf,1024, 0);
		cout << "收到:" << receiveBuf << endl;

		closesocket(serConn);//关闭
		WSACleanup();//释放资源
	}
	return 0;
}

项目运行后截图:
Windows下的socket编程_第4张图片
一些常用的API函数:

  1. 创建套接字——socket()

应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:

SOCKET PASCAL FAR socket(int af, int type, int protocol)

该调用要接收三个参数:af、type、protocol。

参数af指定通信发生的区域:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。

参数type 描述要建立的套接字的类型。这里分三种:
(1)一是TCP流式套接字(SOCK_STREAM)提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
(2)二是数据报式套接字(SOCK_DGRAM)提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
(3)三是原始式套接字(SOCK_RAW)该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。

参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。

  1. 指定本地地址──bind()

当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下:

int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);  

参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。
参数name是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。
参数namelen表明了name的长度。如果没有错误发生,bind()返回0。否则返回SOCKET_ERROR。

  1. 建立套接字连接──connect()与accept()

这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。accept()用于使服务器等待来自某客户进程的实际连接。

connect()的调用格式如下:

int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);

参数s是欲建立连接的本地套接字描述符。
参数name指出说明对方套接字地址结构的指针。
对方套接字地址长度由namelen说明。
如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。

accept()的调用格式如下:

SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);  

参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。
addr指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。
addrlen为客户方套接字地址的长度(字节数)。
如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。

accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr指向一个初始值为空的地址结构,而addrlen的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。

  1. 监听连接──listen()
    此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);

参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。
backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。
如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。
listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。

  1. 数据传输──send()与recv()
    当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。
    send()调用用于s指定的已连接的数据报或流套接字上发送输出数据,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);

参数s为已连接的本地套接字描述符。
buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。
flags指定传输控制方式,如是否发送带外数据等。如果没有错误发生,
send()返回总共发送的字节数。否则它返回SOCKET_ERROR。

recv()调用用于s指定的已连接的数据报或流套接字上接收输入数据,格式如下:

int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);

参数s 为已连接的套接字描述符。
buf指向接收输入数据缓冲区的指针,其长度由len 指定。
flags指定传输控制方式,如是否接收带外数据等。
如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。

  1. 关闭套接字──closesocket()
    closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下:
BOOL PASCAL FAR closesocket(SOCKET s);

参数s待关闭的套接字描述符。
如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。

UDP简介

UDP是一个无连接协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。
虽然UDP是一个不可靠的协议,但它是分发信息的一个理想协议。例如,在屏幕上报告股票市场、显示航空信息等等。在这些应用场合下,如果有一个消息丢失,在几秒之后另一个新的消息就会替换它。UDP广泛用在多媒体应用中。

UDP socket编程实例

  • UDP中的服务器端和客户端没有连接

UDP 不像 TCP,无需在连接状态下交换数据,因此基于 UDP 的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen() 和 accept() 函数。UDP 中只有创建套接字的过程和数据交换的过程。
关于UDP socket的详细编程如下:
客户端:

#include 
#include 
#include 

#pragma comment(lib,"ws2_32.lib")
using namespace std;

int main()
{
	WSADATA wsadata;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsadata))
	{
		cout << "客户端套接字打开失败" << endl;
		return -1;
	}

	SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	SOCKADDR_IN addr1;
	addr1.sin_addr.S_un.S_addr = inet_addr("172.21.32.239");
	addr1.sin_port = htons(6000);
	addr1.sin_family = AF_INET;
	while (true)
	{
		char sendbuf[] = "我是客户端";
		int send = sendto(clientSocket, sendbuf, strlen(sendbuf) + 1, 0, (SOCKADDR*)&addr1, sizeof(SOCKADDR));

		SOCKADDR_IN sevaddr;
		int srvlen = sizeof(SOCKADDR);
		char recvbuf[512];
		memset(recvbuf, 0, 512);
		int recv = recvfrom(clientSocket, recvbuf, 512, 0, (SOCKADDR*)&sevaddr, &srvlen);
		cout << recvbuf << endl;
	}
	closesocket(clientSocket);
	WSACleanup();
	return 0;

}

服务端:

#include 
#include 
#include 

#pragma comment(lib,"ws2_32.lib")
using namespace std;

int main()
{
	WSADATA wsadata;
	if (0!= WSAStartup(MAKEWORD(2, 2), &wsadata))
	{
		cout << "服务端套接字打开失败" << endl;
		return -1;
	}

	SOCKET serSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	SOCKADDR_IN serverAddr;
	serverAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(6000);
	serverAddr.sin_family = AF_INET;

	bind(serSocket, (SOCKADDR*)&serverAddr, sizeof(SOCKADDR));

	
	char buf[512];
	while (true)
	{
		
		memset(buf, 0, 512);

		SOCKADDR_IN clientAddr;
		memset(&clientAddr, 0, sizeof(SOCKADDR));
		int len = sizeof(SOCKADDR);

		int ret = recvfrom(serSocket, buf, 512, 0, (SOCKADDR*)&clientAddr, &len);
		cout << buf << endl;

		sendto(serSocket, "I am a hero.", strlen("I am a hero.") + 1, 0, (SOCKADDR*)&clientAddr, len);
	}
	
	return 0;
}

常用的API:

  1. sendto() 发送数据的函数
int sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen); 

sock:用于传输 UDP 数据的套接字;
buf:保存待传输数据的缓冲区地址;
nbytes:带传输数据的长度(以字节计);
flags:可选项参数,若没有可传递 0;
to:存有目标地址信息的 sockaddr 结构体变量的地址;
addrlen:传递给参数 to 的地址值结构体变量的长度。
UDP 发送函数 sendto() 与TCP发送函数 write()/send() 的最大区别在于,sendto() 函数需要向他传递目标地址信息。

  1. recvfrom() 接收数据的函数
int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen);

sock:用于接收 UDP 数据的套接字;
buf:保存接收数据的缓冲区地址;
nbytes:可接收的最大字节数(不能超过 buf缓冲区的大小);
flags:可选项参数,若没有可传递 0;
from:存有发送端地址信息的 sockaddr 结构体变量的地址;
addrlen:保存参数 from 的结构体变量长度的变量地址值。

封装TCP服务端和客户端类

封装TCP的客户端和服务端类和上边的我写的TCP socket编程实例差不多,就做了稍微的改变,封装的代码如下:
tcp_class.h:

#pragma once

#include 
#pragma comment(lib,"ws2_32.lib")

class TcpServer
{
public:
	//服务端构造函数
	TcpServer();
	//服务端析构函数
	~TcpServer();
	//处理客户端链接
	void handleConnections();
	//处理请求
	static void handleRequests(LPVOID);

private:
	SOCKET TcpSocket;
};

//============================================================

class TcpClient
{

public:
	//客户端构造函数
	TcpClient();
	//客户端析构函数
	~TcpClient();

	/*
		函数名称:Connect
		参数:cIP输入ip地址,iPort是客户端链接的端口
		返回:返回0成功,-1失败
		功能:客户端与服务器进行连接
	*/
	long Connect(const char* cIP);

	/*
		函数名称:SendData
		参数:buf是存储要发送数据,length为数组的长度
		返回:返回0成功,-1失败
		功能:发送数据
	*/
	long SendData(const char* buf, int length);

	/*
		函数名称:RecvData
		参数:buf是存储要发送数据数组,length为数组的长度
		返回:返回0成功,-1失败
		功能:接受数据
	*/
	long RecvData(char* buf, int length);

private:
	SOCKET TcpSocket;//tcp套接字    //连接的端口
};

tcp_class.cpp:

#include "tcp_class.h"
#include 
#include 
#include 

using namespace std;

//构造函数
TcpServer::TcpServer()
{
	WSADATA wsadata;
	if (0 != WSAStartup(MAKEWORD(1, 1), &wsadata))
	{
		cout << "套接字打开失败" << endl;
		return;
	}

	TcpSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	SOCKADDR_IN addr;
	addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	addr.sin_port = htons(6000);
	addr.sin_family = AF_INET;

	bind(TcpSocket, (SOCKADDR*)&addr, sizeof(SOCKADDR));
	listen(TcpSocket, 5);
}

//析构函数
TcpServer::~TcpServer()
{
	closesocket(TcpSocket);
	WSACleanup();
}


//处理客户端链接
void TcpServer::handleConnections()
{
	sockaddr_in remoteAddr;
	int nAddrLen = sizeof(remoteAddr);
	SOCKET currentConnection;
	while (1)
	{
		Sleep(500);
		currentConnection = accept(TcpSocket, (SOCKADDR*)&remoteAddr, &nAddrLen);
		_beginthread(handleRequests, 0, (LPVOID)currentConnection);
	}
}
//处理请求
void TcpServer::handleRequests(LPVOID param)
{
	SOCKET currentConnection = (SOCKET)param;
	char buf[128];
	while (1)
	{
		if (SOCKET_ERROR == recv(currentConnection, buf, 128, 0))
		{
			//可利用WSAGetLastError获取具体错误,并进行相应的错误处理
			//错误类型可参考MSDN

			closesocket(currentConnection);	//断开连接
		}
		else
		{
			//对客户端发来的请求进行解析
			//
			if (SOCKET_ERROR == send(currentConnection, "feedback result", 16, 0))
				cout << "接收的消息有误" << endl;
		}
	}
}


//=============================================================
//初始化客户端
TcpClient::TcpClient()
{
	WSADATA wsa;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
		throw std::exception("加载winsock动态链接库失败!");

	TcpSocket = socket(AF_INET,	//通信协议的协议族,对TCP/IP协议族,该参数设置为AF_INET
		SOCK_STREAM,	//套接字类型为数据流
		IPPROTO_TCP	//通信协议
	);
	if (TcpSocket == INVALID_SOCKET)
		throw std::exception("无效套接字!");

	//设置本地地址
	SOCKADDR_IN localAddr;
	localAddr.sin_family = AF_INET;
	localAddr.sin_port = htons(6000);	//监听端口号
	localAddr.sin_addr.s_addr = INADDR_ANY;	//本机

	if (0 != bind(TcpSocket, (sockaddr*)&localAddr, sizeof(localAddr)))	//绑定地址
		throw std::exception("绑定地址失败!");
}

//客户端析构函数
TcpClient::~TcpClient()
{
	closesocket(TcpSocket);
	WSACleanup();
}

/*
	函数名称:Connect
	参数:ipDest ip地址信息
	返回:返回0成功,-1失败
	功能:客户端链接服务器。
*/
long TcpClient::Connect(const char* ipDest)
{
	//填写远程地址信息
	sockaddr_in servAddr;
	servAddr.sin_family = AF_INET;
	servAddr.sin_port = htons(6000);
	servAddr.sin_addr.S_un.S_addr = inet_addr(ipDest);
	if (connect(TcpSocket, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

/*
	函数名称:SendData
	参数:buf是存储要发送数据,length为数组的长度
	返回:返回0成功,-1失败
	功能:发送数据
*/
long TcpClient::SendData(const char* buf, int length)
{
	if (SOCKET_ERROR == send(TcpSocket, buf, length, 0))
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

/*
	函数名称:RecvData
	参数:buf是存储要发送数据数组,length为数组的长度
	返回:返回0成功,-1失败
	功能:接受数据
*/
long TcpClient:: RecvData(char* buf, int length)
{
	if (SOCKET_ERROR == recv(TcpSocket, buf, length, 0))
	{
		return -1;
	}	
	else
	{
		return 0;
	}
		
}

封装UDP服务端和客户端类

UDP服务端和客户端类的封装和上面的UDP socket编程实例差不多,代码如下:
udp_class.h:

#pragma once
#include 
#pragma comment (lib,"ws2_32.lib")

class Udp
{
public:
	//构造函数初始化
	Udp();
	//析构函数
	~Udp();

	/*
		函数名称:SendData
		参数:ipDest输入ip地址,buf是存储要发送数据数组,len为数组的长度
		返回:ture成功,false失败
		功能:发送数据
	*/
	bool SendData(const char* ipDest, const char* buf, int len);

	/*
		函数名称:RecvData
		参数:buf是存储接受的数据,bufsize是存储数据的大小
		返回:ture成功,false失败
		功能:接收数据
	*/
	bool RecvData(char* buf, int bufsize);

private:
	SOCKET UdpSocket;//udp套接字
};

wdp_class.cpp:

#include 
#include "udp_class.h"

//构造函数
Udp::Udp()
{
	WSADATA wsadata;
	if (0 != WSAStartup(MAKEWORD(1, 1), &wsadata))
	{
		std::cout << "套接字打开失败" << std::endl;
	}
	UdpSocket = socket(AF_INET,     //通信协议的协议族,对TCP/IP协议族,该参数设置为AF_INET
		               SOCK_DGRAM,  //udp套接字类型为数据报套接字
		               IPPROTO_UDP  //udp通信协议           
	                   );
		SOCKADDR_IN addr1;
		addr1.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
		addr1.sin_port = htons(8011);
		addr1.sin_family = AF_INET;

		if (0 != bind(UdpSocket, (SOCKADDR*)&addr1, sizeof(SOCKADDR)))
		{
			std::cout << "bind failed..." << std::endl;
		}

}

//析构函数
Udp::~Udp()
{
	closesocket(UdpSocket);
	WSACleanup();
}

/*
	函数名称:SendData
	参数:ipDest输入ip地址,buf是存储要发送数据数组,len为数组的长度
	返回:ture成功,false失败
	功能:发送数据
*/
bool Udp:: SendData(const char* ipDest, const char* buf, int len)
{
	SOCKADDR_IN destaddr;
	destaddr.sin_addr.S_un.S_addr = inet_addr(ipDest);
	destaddr.sin_port = htons(8011);
	destaddr.sin_family = AF_INET;
	if (0 != sendto(UdpSocket, buf, len, 0, (SOCKADDR*)&destaddr, sizeof(SOCKADDR)))
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

/*
	函数名称:RecvData
	参数:buf是存储接受的数据,bufsize是存储数据的大小
	返回:ture成功,false失败
	功能:接收数据
*/
bool Udp:: RecvData(char* buf, int bufsize)
{
	SOCKADDR_IN fromaddr;
	int length = sizeof(SOCKADDR);
	if (0 != recvfrom(UdpSocket, buf, bufsize,0,(SOCKADDR*)&fromaddr,&length))
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

问题和解决

tcp socket编程一对一聊天出现“烫烫烫”的问题。

我在tcp socket一对一聊天实例的编程过程中出现了前几个数据能正常运行,但是超过一定空间后就会出现好多“烫烫烫”,这个问题困扰了三天,在这三天我对我的程序改了又改,还是没改对重点。之前出现的bug如下图所示:

Windows下的socket编程_第5张图片
我曾经以为是数组被赋值后没有初始化的问题,结果当我每次循环对数组初始化后,该问题还是存在。真是离了大谱。后来我又认为是我收发数据循环放的位置不对,结果试了好几次不同的循环之后,这个bug丝毫不受影响。直到今天偶然间才发现是send()和recv()函数中参数len的问题。
出bug程序代码:

    char recBuf[BUFSIZ];
	char senBuf[BUFSIZ];
	SOCKADDR_IN addr1;
	int length = sizeof(SOCKADDR);
	SOCKET serCon = accept(serSocket, (SOCKADDR*)&addr1, &length);
	while(1)
	{
		//recv(serCon, recBuf, BUFSIZ, 0);
		recv(serCon, recBuf, strlen(recBuf), 0);
		cout << "输出的内容为:" << recBuf << endl;
		cout << "输入的内容为:";
		gets_s(senBuf, BUFSIZ);
		//send(serCon, senBuf, BUFSIZ, 0);
		send(serCon, senBuf, strlen(senBuf), 0);
	}

send()函数和recv()函数中的第三个参数len即buf 参数所指向的缓冲区中数据的长度(以字节为单位)
Windows下的socket编程_第6张图片
把这个参数改成确定的大小就不会出错了。
代码如下:

char recBuf[BUFSIZ];
	char senBuf[BUFSIZ];
	SOCKADDR_IN addr1;
	int length = sizeof(SOCKADDR);
	SOCKET serCon = accept(serSocket, (SOCKADDR*)&addr1, &length);
	while(1)
	{
		recv(serCon, recBuf, BUFSIZ, 0);
		//recv(serCon, recBuf, strlen(recBuf), 0);
		cout << "输出的内容为:" << recBuf << endl;
		cout << "输入的内容为:";
		gets_s(senBuf, BUFSIZ);
		send(serCon, senBuf, BUFSIZ, 0);
		//send(serCon, senBuf, strlen(senBuf), 0);
	}

Windows下的socket编程_第7张图片

TCP粘包和分包解决

  1. 出现粘包的原理

我们要发送两个hello数据,一个hello占5个,TCP假如一次性传输能存10个。当第一个hello存进TCP的缓存区里面时,没有存满,还剩下5个空位,这时第二个hello过来,刚好占满剩下的5个,然后这两个hello就粘在一起了,变成hellohello了。

图解:
Windows下的socket编程_第8张图片

  1. 出现分包的原理

我们要发送两个hello,一个hello要占领5个空位。但是TCP的一个包只有4个空位,。这时第一个hello传过来,只存了hell,剩下的e被分到下一个包存储,所以就成了分包。

图解:
Windows下的socket编程_第9张图片

Windows下的socket编程_第10张图片

3.解决方案

  • 自定义报文格式
    即报文长度+报文内容,如:

0010abcdefghij
其中0010 是报文内容的长度,abcdefjhij为报文内容。
Windows下的socket编程_第11张图片

实例:
客户端代码:

int ret = 0;
	do
	{
		char buf1[100] = { 0 };
		//1 2 3 0 0 0 0 00 0 ....
		//3 1 2 3 00000000.....
		buf1[0] = 3;    //buf1  3 0 0 0 0...
		strcpy(&buf1[1], "123");//buf1 3 1 2 3 0 ....
		ret = send(s, buf1, 4, 0);//buf1 3 1 2 3 0 .....
	} while (ret != SOCKET_ERROR && ret != 0);

服务端代码:

int ret = 0;
	do
	{
		char buf2[100] = { 0 };               //buf2 0 0 0 0 0 ....
		ret = recv(c, buf2, 100, 0);          //buf2  3 1 2 3 0 ....
		int nLen = buf2[0];                   //nLen buf的长度为3 
		char chRealData[100] = { 0 };         //chRealData 0 0 0 0......
		memcpy(chRealData, &buf2[1], nLen);   //复制buf2[1] 1,buf[2] 2,buf[3] 3 到chRealData中,chRealData="123"
		cout << "客户端传输的信息为:" << chRealData << endl;//输出123
	} while (ret != SOCKET_ERROR && ret != 0);

你可能感兴趣的:(C++,windows,udp,网络)