13.Win下socket编程(笔记)

1 概述

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

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

2 Socket的主要类型

2.1 流套接字(SOCK_STREAM)

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

2.2 数据报套接字(SOCK_DGRAM)

数据报套接字提供一个无连接的服务。该服务不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。

2.3 原始套接字(SOCT_RAW)

原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:

原始套接字可以读取内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接字。

3 TCP/IP三次握手建立链接

当你想要理解socket,你就必须先理解tcp/ip,它们之间好比送信的线路和驿站的作用。

TCP/IP 协议不同于iso的7个分层,他是根据这7个分层,将其重新划分,归类到四个抽象层中:

  • 应用层:TFTPHTTPSNMPFTPSMTPDNSTelnet
  • 传输层:TCPUDP
  • 网络层:IPICMPOSPFEIGRPIGMP
  • 数据链路层:SLIPCSLIPPPPMTU

每一个抽象层建立在低一层提供的服务上,并且为高一层提供服务,如下图:

在这里插入图片描述

那么究竟TCP/IP协议是怎么通讯建立链接的?这就四我要讲的三次握手。

TCP协议通过三个报文段完成连接的建立,这个过程称为三次握手,过程如下图所示。

在这里插入图片描述

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN-RECV状态。

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

一次完整的三次握手也就是:请求—应答—再次确认。

4 TCP socket编程实例

4.1 编程步骤

下图展示了TCP socket编程的步骤:

在这里插入图片描述

服务器端:

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

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

客户端:

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

4.2 代码实例

客户端代码:

#include <iostream>
#include <winsock2.h>
#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 <winsock2.h>
#include <stdio.h>
#include <iostream>
#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;
		}
        //parm1:af 地址协议族 ipv4 ipv6
        //parm2:type 传输协议类型 流式套接字(SOCK_STREAM),数据包套接字(SOCK_DGRAM)
        //parm3:ptotoc1 使用具体的某个传输协议
		SOCKET serSocket = socket(AF_INET, SOCK_STREAM, 0);//创建可识别的套接字
		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;
}

4.3 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:描述要建立的套接字的类型。这里分三种:
    • TCP流式套接字(SOCK_STREAM)提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
    • UDP数据报式套接字(SOCK_DGRAM)提供了一个无连接服务。数据包以独立包形式被发送,不提供无措保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
    • 原始式套接字(SOCK_RAW)该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
  • 参数protocol:说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。

根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议“这一元。

2. 指定本地地址—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

3. 建立套接字连接—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()用于面向连接服务器。参数addraddrlen存放客户方的地址信息。

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

4. 监听链接—listen()

此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:

int PASCAL FAR listen(SOCKET s, int backlog);
  • 参数s,标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求,

  • 参数backlog,表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。

  • 返回值:

    如果没有发生错误,listen返回0,否则它返回SOCKET_ERROR。

listen()在执行调用过程中,可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。

5. 数据传输—send()与recv()

当一个连接建立以后,就可以传输数据了。常用的系统调用有send()recv()

send()调用用于s指定的已连接的数据报或流套接字上发送输出数据,格式如下:

int PASCAL FAR send(SOCKET s,const char FAR *buf,int len,int flags);
  • 参数s:已连接的本地套接字描述符。

  • 参数buf:指向存有发送数据的缓冲区的指针,其长度由len指定。

  • 参数flags:指定传输控制方式,如是否发送带外数据等。如果没有错误发生。

  • 返回值:

    返回总共发送的字节数,否则返回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。

6.关闭套接字—closesocket()

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

BOOL PASCAL FAR closesocket(SOCKET s);
  • 参数s:待关闭的套接字描述符。

  • 返回值:

    如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR

5 UDP 概述

UDP是一个无连接协议,传输数据之前源端和终端不建立连接,当它想传送时,就简单地去抓取来自应用程序的数据,并尽可能块地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放到队列中,应用程序每次从队列中读一个消息段。

由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。

虽然UDP是一个不可靠的协议,但它是分发消息的一个理想协议。例如,在屏幕上报告股票市场、显示航空信息等等。在这些应用场合下,如果有一个消息丢失,在几秒之后另一个新的消息就会替换它。UDP广泛用在多媒体应用中。

6 UDP socket编程实例

6.1 代码实例

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

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

关于UDP socket的详细编程如下:

客户端:

#include <winsock2.h>
#include <iostream>
#include <stdio.h>

#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;
	
	bind(clientSocket, (SOCKADDR*)&addr1, sizeof(SOCKADDR));
	
	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 <winsock2.h>
#include <iostream>
#include <stdio.h>

#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;
}

6.2 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()函数需要向他传递目标地址信息。

2. 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的结构体变量长度的变量地址值。

7 封装TCP服务端和客户端类

封装TCP的客户端和服务端类,和上边的TCP socket编程实例相似,封装的代码如下:

tcp_class.h

#pragma once

#include <winsock2.h>
#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 <iostream>
#include <stdio.h>
#include <process.h>

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;
	}
		
}

8 封装UDP服务端和客户端类

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

#pragma once
#include <winsock2.h>
#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套接字
};

udp_class.cpp

#include <iostream>
#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;
	}
}

9 TCP粘包和分包解决

9.1 出现粘包的原理

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

在这里插入图片描述

9.2 出现分包的原理

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

在这里插入图片描述
在这里插入图片描述

9.3 解决方案

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

0010abcdefghij

其中0010 是报文内容的长度,abcdefghij 为报文内容。

char context[101];
strcpy(context,"abcdefghi");		//待发送的报文内容

int iLen = strlen(context);			//待发送报文的长度

char TBuffer[iLen+4];				//发送缓冲区
memset(TBuffer,0,sizeof(TBuffer));	//清理发送缓冲区

memcpy(TBuffer,&iLen,4);			//把报文长度拷贝到缓冲区
memcpy(TBuffer+4,context,iLen);		//把报文内容拷贝到缓冲区

实例:

客户端代码:

int ret = 0;
do
{
    char buf1[100] = {0};
    //1 2 3 0 0 0 0 0.....
    //3 1 2 3 0 0 0 0.....
    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,buf2[2] 2,buf2[3] 3到chRealData中,chRealData="123"
    cout << "客户端传输的信息为:" << chRealData << endl;		//输出123
}while(ret != SOCKET_ERROR && ret != 0)

10 单播/广播/组播 的概念

10.1 单播

之前在进行UDPTCP编程的时候,客户端把数据发送到指定IP地址,此时接收方只有一个,这种数据报的发送方式成为“单播”。

在这里插入图片描述

10.2 广播

如果是把数据发送某个局域网中的所有主机,这种数据包的发送方式称为"广播"。

  • 发送出去的数据会被 广播地址所在网段的所有主机接收

  • 每个局域网的最大主机地址代表该网段的广播地址。

    以192.168.1.0(255.255.255.0)网段为例,192.168.1.255代表该网段的广播地址

  • 255.255.255.255 在所有网段中都代表广播地址。

    如果主机A向 255.255.255.255 发数据,那么当前局域网里的所有主机都会收到数据。

在这里插入图片描述

10.3 多播(组播)

如果是把数据发送给某个局域网中的一组IP地址,这种发送方式称为“多播”,这个组称为“多播组”,只有加入多播组的主机才能收到数据。

  • 广播是发给某一局域网中的所有主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常通信。
  • 多播是一种折中的方式,既能发给多个主机,又能避免像广播那样带来过多的负载。

在这里插入图片描述

11 广播 socket编程(只能是UDP通信)

广播socket编程的侧重点在发送方(客户端),而且只有使用UDP协议才能广播。接收方的代码可以沿用单播的代码。发送方的数据发送步骤如下:

  • 创建用户数据报套接字
  • 套接字默认不允许广播数据包(因为可能引发广播风暴),需要使用setsockopt设置属性
  • 目标地址(接收方地址)指定为广播地址
  • 指定目标端口
  • 发送数据包
#define DST_PORT 9090
 
/* 1. 创建socket fd */
if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) {	//UDP编程
    perror ("socket");
    exit (1);
}
 
/* 2. 允许广播设置 */
int b_br = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &b_br, sizeof(int));
 
/*3. 指定目标IP和端口号填充 */
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_port = htons (SERV_PORT);	                  //网络字节序的端口号
sin.sin_addr.s_addr = inet_addr("192.168.11.255");    // 广播地址
 
 
/*4. 发送数据 */
char buf[128] = {'1','2','3','4','5','6','\0'};
sendto (fd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin));

12 多播 socket编程(只能是UDP通信)

多播socket编程的侧重点在接收方(服务端),接收方要创建一个多播组(类似于QQ群),然后把当前套接字加入到多播组中;而发送方发送数据的目标地址不是服务端的IP地址,而是多播组的IP地址。

  • 创建数据报套接字
  • 绑定IP地址和端口号
  • 创建多播组(类似于创建QQ群)
  • 将当前套接字加入到多播组(类似于加群)
  • 接收数据
#define SERV_PORT 9090
#define MULTICAST_IP "192.168.11.170"
#define BUFSIZE 128
 
int fd = -1;
 
/* 1. 创建socket fd */
if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) {	//udp程序
    perror ("socket");
    exit (1);
}
 
/* 2. 绑定IP地址和端口号 */
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);	         // 网络字节序的端口号
sin.sin_addr.s_addr = htonl(INADDR_ANY);    // 服务端可以绑定任意IP
if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
    perror ("bind");
    exit (1);
}
 
/* 3. 创建多播组,初始化多播组结构体 */
struct ip_mreq mreq;
memset(&mreq, 0, sizeof(mreq))
mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_IP);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
 
/* 4. 把当前套接字加入到多播组 */
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
 
/* 5. 接收数据 */
char buf[BUFSIZE];
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
recvfrom (fd, buf, BUFSIZE - 1, 0, (struct sockaddr *)&cin, &addrlen);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值