13.Win下socket编程(笔记)

Windows C/C++ Socket 编程 作者很懒,本文没有摘要。 阅读详情
AI摘要

本文围绕Win下socket编程展开,介绍了套接字概念、主要类型,如流套接字、数据报套接字等。阐述了TCP/IP三次握手建立连接过程,给出TCP和UDP socket编程实例及API接口。还提及TCP粘包和分包问题及解决办法,以及单播、广播、组播概念和对应编程方法。

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个分层,将其重新划分,归类到四个抽象层中:

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

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

在这里插入图片描述

那么究竟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()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。

  • 调用前,参数addr指向一个初始值为空的地址结构,而addrlen的初始值为0;
  • 调用后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求时有客户方的connect()调用发出的。
  • 当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr和addrlen,并创建一个与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 单播

之前在进行UDP和TCP编程的时候,客户端把数据发送到指定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);
Windows -- UDP Socket 1、服务端代码 //========================================== // Filename : UDP_Send_Message_Server.cpp // Time : 2019年8月13日 // Author : 柚子树 // Email : gz_du... 阅读详情

相关推荐

TCP 三次握手与四次挥手:从Wireshark抓包到C语言Socket编程的5个关键实现

本文深入解析TCP协议的三次握手与四次挥手过程,结合Wireshark抓包分析和C语言Socket编程实践,详细展示了从报文层面到代码实现的关键技术。通过5个核心实现步骤,帮助开发者掌握TCP连接建立与终止的底层机制,提升网络编程能力与问题排查效率。

weixin_30878501的博客 324

Windows下的socket编程

socket编程是网络常用的编程,我们通过在网络中创建socket关键字来实现网络间的通信。那么socket到底是什么呢?百度百科给了以下解释所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。...........................

StarWang的博客 1万+

国产信创服务器软硬件兼容情况1 --- ARM架构

信创,即信息技术应用创新产业,它是数据安全、网络安全的基础,也是新基建的重要组成部分...

一只猿的自我修养 4741

Windows——socket客户端与服务端

作者:小 琛 欢迎转载,请标明出处 Windows下的socket与Linux下有些许不同,有关Windows下socket初始化内容,翻看前篇博客: Windows下socket初始化 Windows-socket客户端与服务端编程流程相关函数创建套接字绑定套接字侦听套接字连接套接字接受连接发送、接收数据断开连接实战 客户端与服务端编程流程 相关函数 创建套接字 网络编程中,绕不开的一个话题:字节序问题 addrinfo 结构体 typedef struct addrinfo { int

qq_44745063的博客 3623

AI大模型学习(13)小白也能学会的10分钟豆包大模型的python教程1

豆包AI大模型API是字节跳动推出的智能对话接口服务,开发者可以通过简单的HTTP请求调用强大的AI能力,实现智能问答、内容生成等功能。

isoft888的专栏,致力于C#/python分享物联网、大数据,神经网络的等领域,欢迎讨论交流! 1001

SAP License:SAP会计凭证抬头的字段状态控制

FI模块的同事一般都知道在FI中有个字段状态变式,可以控制在凭证输入时哪些字段是必输、哪些可选哪些隐藏,但这只对凭证行项目中有效,你是否想过对于凭证抬头的字段是哪控制呢? 在中国,会计附件上都要带一个附件张数字段,但SAP的默认配置中没有这一字段,并非SAP没有设置此字段,而是我们没有配置这一字段。如果需要在凭证输入时放出分支号(BRNCH)、附件页数(NUMPG)等字段,在个人参数文件中加入参数CSF,参数值AR,即可。如下图: 实际上,这是对凭证国家的一个设置,在SAP中,配置了AR这个国家..

SAP权限管控|SAP用户权限管理|SAP权限合规检查|SAP全日志管理 2225

Windows socket基础

Windows socket 基础        Windows socket是一套在Windows操作系统下的网络编程接口。它不是一种网络协议,而是一个开放的、支持多个协议的Windows下的网络编程接口 。      Windows socket是以Unix socket为基础,因此Windows socket中的许多函数名与Unix都是一样的。除此之外它还允许开发人员充分利用Wind

ithzhang 2万+

Transformer数学推导——Q38 旋转位置编码(RoPE)的复数形式 对注意力分数的几何影响分析

RoPE 的复数形式为自然语言处理中的位置编码提供了创新且有效的解决方案。通过深入的数学理论分析,明确了其对注意力分数的几何影响及在捕捉位置信息方面的优势。在 LLM 的实际应用中,RoPE 复数形式在多个任务和场景中展现出强大的性能,特别是在处理长序列文本时,能够有效捕捉长距离依赖、缓解位置编码局限性并提升处理效率。然而,RoPE 复数形式也面临计算复杂度高、超参数调整困难和位置信息平衡等挑战。通过合理的优化策略和与其他技术的结合,有望进一步提升其性能。

墨顿随笔 1419

Sockets是Windows下网络编程的规范,Windows Sockets是Windows下得到广泛应用的、开放的、支持多种协议的网络编程接口。详细用法讲解如下

C#sockets用法详解。

xzs1955的博客 1235

[特殊字符] RK3588 平台上基于 Panfrost 的 Mesa 驱动编译全过程

摘要: 本文详细记录了在RK3588平台(Ubuntu 20.04 aarch64)上编译基于Panfrost的Mesa驱动的全过程。内容包括:1)系统依赖安装;2)libdrm编译;3)Mesa配置参数优化;4)LLVM 15与SPIRV工具链的安装方法;5)SPIRV-Tools版本问题的解决方案;6)最终编译命令及常见错误处理。关键点在于正确设置工具链路径(PKG_CONFIG_PATH)和版本匹配(LLVM 15对应SPIRV-LLVM-Translator v15.0.0),推荐将组件安装在/op

梁俊星的博客 941

MobileViT替换YOLOv11主干网络:轻量化新思路-(用MobileViT等轻量视觉Transformer降低计算成本)

在计算机视觉领域,目标检测一直是核心任务之一,而YOLO(You Only Look Once)系列算法因其出色的速度-精度平衡成为工业界和学术界的热门选择。随着YOLOv11的推出,模型性能达到了新的高度,但其计算复杂度和参数量也随之增加,这对资源受限的移动设备和嵌入式系统提出了挑战。与此同时,视觉Transformer(ViT)的出现为计算机视觉带来了全新范式,但其庞大的计算需求限制了在移动端的应用。MobileViT作为一种创新的轻量级视觉Transformer架构,巧妙结合了CNN的归纳偏置优势和V

本博客聚焦 YOLOv11 全流程落地,涵盖架构优化、数据集处理、训练技巧与多场景部署,兼及国产数据库、Java 开发与 AI 模型应用,内容兼顾理论与工程实践,为开发者提供系统干货,助力高效提升技术能力。 1647

c语言socket编程windows和Linux下的区别

1)头文件 windows下winsock.h/winsock2.h linux下sys/socket.h,netinet/in.h 错误处理:errno.h 2)初始化 windows下需要用WSAStartup WSADATA wsaData; err = WSAStartup(0x202,&wsaData); if ( err != 0 ) { return 0...

qq_45337977的博客 977

educoder实训——计算几何形状的表面积与体积

educoder python实训

qq_41234663的博客 1230

windows socket简单编程示例

windows下的socket编程和Linux下的socket编程很相似,但也有些不同,转载的简单示例,供大家参考。有错误的地方望指点! 这样的代码网上很多,以下就采用了孙鑫VC++第14讲的代码,下面是添加的一点简单注释。       其实socket完成连接功能很简单,依葫芦画瓢即可。首先来看面向连接的TCP流程图。              面向无连接的UDP流程

overlord_bingo的博客 510

SDR 和 DDR 的区别

SDR 和 DDR 的差别,最明显提现在传输速率和频率上。DDR SDRAM与SDRAM的基本结构是相似的,最根本的区别在于DDR 支持在一个时钟周期内传输两次数据,,SDR 只能传输一次数据。

串起点滴 1961

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

win7下libevent实现的rot13服务器

// libev_rot13.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include #include #define MAX_LINE 16384 void do_read(evutil_socket_t fd, short events, void *arg)

huyinhou 757

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Win Socket编程原理及简单实例

[转]http://www.cnblogs.com/tornadomeet/archive/2012/04/11/2442140.html  使用Linux Socket做了小型的分布式,如Linux C Socket编程原理及简单实例。 为了更好地分布也得看看Win Socket。 Win Socket TCP原理图: Win Socket UDP原理图:    简单TCP连...

weixin_34006965的博客 282

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

win socket编程下连接错误 VC6.0

在VC6.0下写了一个简单的基于TCP的socket程序,但是在连接时出现如下错误:     --------------------Configuration: TCPSrv - Win32 Debug-------------------- Linking... main.obj : error LNK2001: unresolved external symbol __imp__c

starmlk的专栏 831

网络编程实验一 win socket基础 获取服务器时间

  花了十个小时左右上网搜罗资料,写代码,改bug才写出了这么个程序,真是感慨对winsocket编程真是一窍不通啊。   先推荐本人认为比较不错的参考书:《Visual C++网络编程经典案例详解》《WinSock网络编程经络》,后者我在学校图书馆借到了,开篇就是我们第一个实验。   操作系统 win8   语言:c++   开放框架:QT   配置问题,很多同学...

a973356183的博客 275

TCP的socket连接示例(win/unix 非阻塞)

http://www.zeali.net/blog/entry.php?id=13

haven的专栏 971

socket+mysql

测试windows 下 Socket+Mysql调用。 环境: OS:Win7; DBMS:Mysql; IDE:codeblockS. server: 1 #include <WINSOCK2.H> 2 #include <stdio.h> 3 #include <mysql.h> 4 /*数据库连接用宏*/ 5 #...

weixin_30449239的博客 186

Windows下C语言的Socket编程例子(TCP和UDP)

一。   server端: 1 #include "stdafx.h" 2 #include 3 #include 4 5 #pragma comment(lib,"ws2_32.lib") 6 7 int main(int argc, char* argv[]) 8 { 9 //初始化WSA 10 WORD sockVersion = M

killerover84的专栏-----天下 990

C-socket编程-多进程版并发服务器

#include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #include "wrap.h"int main() { //创建套接字 int serv_sock = Socket(AF_INET,SOCK_STREAM,0); /

wwzhang0307的博客 3221

Glue4Net简单部署基于win服务的Socket程序

在写一些服务应用的时候经常把要它部署到windows service中,主要是让系统自动加载相关程序用于后台运行.正常写windows server需要单独写个程序包装和相应的安装类才能完成这些工作.但通过Glue4Net就可以非常简单地把代码或DLL部署到windows server中运行.下面地介绍通过Glue4Net简单地部署一个Socket程序. 安装Glue4net 首要工...

dcnna0422的博客 113

win8 metro socket

1 private async void btnListen_Click(object sender, RoutedEventArgs e) 2 { 3 try 4 { 5 _listener = new StreamSocketListener(); 6 ...

weixin_30535043的博客 68
上一篇: 12.2 Boost-Asio(笔记)
下一篇: 14.Boost-内存管理(笔记)
蜡笔小马
博客等级 码龄11年 431粉丝 98原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值