【网络编程】套接字编程——UDP通信

news/2024/5/18 15:14:13 标签: 网络, udp, 网络协议

文章目录

  • 一、预备知识
    • 1. 源IP地址和目的IP地址
    • 2. 源MAC地址和目的MAC地址
    • 3. 端口号PORT和进程标识符PID
  • 二、TCP协议和UDP协议
    • 1. TCP协议和UDP协议
    • 2. 网络字节序
  • 三、socket编程
    • 1. 套接字常见的API
    • 2. 套接字的数据结构
  • 四、UDP服务器
    • 1. echo服务器
    • 2. bash服务器
    • 3. 简易的公共聊天室
  • 五、套接字相关知识梳理


一、预备知识

1. 源IP地址和目的IP地址

因特网上的每台计算机都有一个唯一的IP地址,如果一台主机上的数据要传输到另一台主机,那么对端主机的IP地址就应该作为该数据传输时的目的IP地址。但仅仅知道目的IP地址是不够的,当对端主机收到该数据后,对端主机还需要对该主机做出响应,因此对端主机也需要发送数据给该主机,此时对端主机就必须知道该主机的IP地址。因此一个传输的数据当中应该涵盖其 源IP地址目的IP地址,目的IP地址表明该数据传输的目的地,源IP地址作为对端主机响应时的目的IP地址。


2. 源MAC地址和目的MAC地址

在实际生活中,大部分数据的传输都是跨局域网的,数据在传输过程中会经过若干个路由器,最终才能到达对端主机。

源MAC地址目的MAC地址 是包含在链路层的报头当中的,而MAC地址实际只在当前局域网内有效,因此当数据跨网络到达另一个局域网时,其源MAC地址和目的MAC地址都会发生变化,因此,当数据到达路由器时,路由器会将该数据当中链路层的报头去掉。然后再重新封装一个报头,此时该数据的源MAC地址和目的MAC地址就发生变化了。

因此数据在传输的过程中是有两套地址的:

  • 源IP地址和目的IP地址: 这两个地址再数据传输的过程中基本是不会发生变化的(存在一些特殊情况,比如在数据传输过程中使用NET技术,其源IP地址会发生变化,但至少目的IP地址是不会发生变化的)
    源MAC地址和目的MAC地址: 这两个地址是一直在发生变化的,因为在数据传输的过程中,路由器不断在解包和重新封装。

3. 端口号PORT和进程标识符PID

两台主机之间通信的目的是为了访问对端主机上的某个服务,实际上通信是为了两台主机上的两个不同的服务进程之间进行数据传输,因此socket通信的本质是 两个进程之间在通信,只不过在这里的进程通信是两个跨网络的进程之间在通信。

因此进程间通信的方式除了管道、消息队列、信号量、共享内存等不跨网络的通信方式,还有跨网络的进程间的通信方式——套接字

💕 端口号

  • 端口号(port)是传输层协议的内容.
  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

因为端口号是隶属于某台主机的,所以端口号可以在两台不同的主机当中重复,但是在同一台主机上进行网络通信的进程的端口号不能重复。此外,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。


端口号PORT和进程标识符PID

我们知道,进程PID也能够唯一标识一台主机上的某个进程,那为什么在网络通信时嗨哟啊创建端口号呢?

这时因为一台机器上可能有大量的进程,但并不是所有的进程都需要进行网络通信的,可能存在很多的进程是不需要进行网络通信的本地进程,此时虽然PID也可以标识这些网络进程的唯一性,但是在该场景下就不太合适了。

因此,在不同的场景下可能需要不同的编号来标识某种事物的唯一性,因为这些编号更适用于该场景。


二、TCP协议和UDP协议

网络协议栈是贯穿整个体系结构的,在应用层、操作系统层和驱动层各有一部分。当我们使用系统调用接口实现网络数据通信时,必须通过传输层,在传输层最重要的两种协议就是TCP协议UDP协议

1. TCP协议和UDP协议

💕 TCP协议

TCP(Transmission Control Protocol) 协议叫做传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。

下面是TCP协议的一些特点和功能:

  • 可靠性: TCP提供可靠的传输机制,通过序列号、确认应答和重传等方式来确保数据的正确传输。

  • 面向连接: 在数据传输之前,发送方和接收方必须建立一个TCP连接。连接的建立和终止需要经过三次握手和四次挥手过程。

  • 字节流传输 TCP将数据流视为一系列的字节流,没有固定的消息边界。发送方将数据分割为适当的大小的TCP段发送,接收方负责重新组装原始数据。

  • 流量控制: TCP使用滑动窗口机制来控制数据的发送速率,确保发送方不会淹没接收方,从而实现流量控制。

  • 拥塞控制: TCP通过拥塞窗口和拥塞信号来控制网络拥塞。它会根据网络的拥塞程度自适应地调整发送速率,以避免网络拥塞。

  • 可靠的顺序交付: TCP确保接收方按正确的顺序接收数据,即使数据在传输过程中可能以不同的顺序到达。

  • 可全双工通信: TCP连接是全双工的,意味着发送方和接收方可以同时进行数据传输。

💕 UDP协议

UDP协议 叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。

使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。

下面是UDP的一些关键特点和功能:

  • 无连接性: UDP是一种无连接的协议,发送方无需在传输数据之前与接收方建立连接。每个UDP数据包(称为数据报)都是独立的,可以独立地发送和接收。

  • 低延迟: 由于没有连接建立和维护的开销,UDP具有较低的延迟。这使得它适用于需要实时性和快速传输的应用,如音频和视频流。

  • 不可靠性: UDP不提供可靠性保证。发送的数据报可能丢失、重复或乱序到达,接收方无法检测这些问题。如果应用程序对数据丢失或传输顺序没有严格要求,UDP就是一个合适的选择。

  • 支持广播和多播: UDP协议支持向多个目标发送数据报,包括广播(一对多)和多播(一对多或多对多)通信。

  • 面向数据报: UDP的数据报头相对较小,封装开销较低。它将应用程序发送的数据添加到数据报中,并在传输过程中保持原样。

  • 不提供流量控制和拥塞控制: 由于UDP协议不提供流量控制和拥塞控制机制,发送方可以按照自己的速率发送数据。这也意味着在网络拥塞或高负载情况下,UDP的性能可能会受到影响。

既然UDP协议是一种不可靠的传输协议,UDP协议存在的意义是什么呢?

TCP协议的可靠性是需要付出一定代价的,它的底层实现是比较复杂的。同样UDP协议虽然是一种不可靠的传输协议,但这一定意味着UDP协议在底层不需要做过多的工作,因此UDP协议底层的实现一定比TCP协议要简单。UDP协议虽然不可靠,但是她能够快速的将数据发送给对方,虽然在数据传输的过程中可能会出错。

编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单。


2. 网络字节序

如果编写的程序只在本地机器上运行,那么是不需要考虑大小端问题的,因为同一台机器上的数据采用的存储方式都是一样的,要么采用的都是 大端存储模式,要么采用的都是 小端存储模式。但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不一致的。

在这里插入图片描述

由于我们不能保证通信双方存储数据的方式是一样的,因此网络当中传输的数据必须考虑大小端问题。因此TCP/IP协议规定,网络数据流采用大端字节序,即低地址高字节。无论是大端机还是小端机,都必须采用TCP/IP协议规定的网络字节序来发送和接收数据。

  • 如果发送端是小端,需要先将数据转成大端,然后再发送到网络当中。
  • 如果发送端是大端,则可以直接进行发送。
  • 如果接收端是小端,需要先将接收到数据转成小端后再进行数据识别。
  • 如果接收端是大端,则可以直接进行数据识别。

需要注意的是,所有的大小端的转化工作是由操作系统来完成的,因为该操作属于通信细节,不过也有部分的信息需要我们自行进行处理,比如 端口号IP地址

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 函数名当中的h表示hostn表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位长整数从主机字节序转换为网络字节序。
  • 如果主机是小端字节序,则这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,则这些函数不做任何转换,将参数原封不动地返回。

三、socket编程

1. 套接字常见的API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

在这里插入图片描述


2. 套接字的数据结构

在这里插入图片描述

套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中 sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。

为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_insockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族

此时当我们在传递在传参时,就不用传入sockeaddr_insockeaddr_un这样的结构体,而统一传入sockeaddr这样的结构体。在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信,在这些API内部就可以提取sockeaddr结构头部的16位进行识别,进而得出我们是要进行网络通信还是本地通信,然后执行对应的操作。此时我们就通过通用sockaddr结构,将套接字网络通信和本地通信的参数类型进行了统一。

注意: 实际我们在进行网络通信时,定义的还是sockaddr_in这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr*罢了。

四、UDP服务器

1. echo服务器

udp_server.hpp

#pragma once
#include "err.hpp"
#include <iostream>
#include <memory>
#include <string>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

namespace ns_server
{
	const static uint16_t default_port = 8080; 
	class UdpServer
	{
	public:
		UdpServer(uint16_t port = default_port)
			:port_(port)
		{}

		void InitServer()
		{
			// 创建套接字接口, 打开文件
			sock_ = socket(AF_INET, SOCK_DGRAM, 0);
			if(sock_ < 0)
			{
				cerr << "create socket error: " << strerror(errno) << endl;
				exit(SOCKET_ERR);
			}
			cout << "create socket success: " << sock_ << endl;

			// 给服务器指明IP地址和端口号
			struct sockaddr_in local;
			bzero(&local, sizeof local);
			local.sin_family = AF_INET; // 将16位地址类型设置为网络通信
			local.sin_port = htons(port_); // 将主机的端口号转为网络序列

			// local.sin_addr.s_addr = inet_addr(ip_.c_str());
			local.sin_addr.s_addr = INADDR_ANY;
			// 将套接字字段和文件字段进行绑定关联
			if(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				std::cerr << "bind socket error: " << strerror(errno) << std::endl;
				exit(BIND_ERR);
			}
			std::cout << "bind socket success: " << sock_ << std::endl;

		}

		void Start()
		{
			char buffer[1024];
			while(true)
			{
				// 获取用户数据报
				struct sockaddr_in peer;
				socklen_t len = sizeof(peer);
				int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
				if(n > 0)
					buffer[n] = '\0';
				else
					continue;
				// 将4字节的IP转换成字符串IP
				string clientip = inet_ntoa(peer.sin_addr); // 客户端IP

				// 将网络序列转换为主机序列
				uint16_t clientport = ntohs(peer.sin_port); // 客户端端口号

				cout << clientip << "-" << clientport << "-get message " << buffer << endl;

				// 将消息发回客户端
				sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, sizeof(peer));

			}
		}


		~UdpServer()
		{}
	private:
		int sock_; // 套接字文件描述符
		uint16_t port_; // 端口号
	};
}

udp_server.cc

#include "udp_server.hpp"

using namespace ns_server;

static void usage(string proc)
{
	cout << "usage:\t" << proc << "port\n" << endl;
}

int main(int argc, char* argv[]) // 命令行参数
{
	if(argc != 2)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}

	// 端口号
	uint16_t port = atoi(argv[1]);

	unique_ptr<UdpServer> usvr(new UdpServer(port));

	usvr->InitServer();
	usvr->Start();

	return 0;
}

udp_client.cc

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
using namespace std;

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; 
}

int main(int argc, char* argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}
	string serverip = argv[1];
	uint16_t serverport = atoi(argv[2]);

	int sock = socket(AF_INET, SOCK_DGRAM, 0);
	if(sock < 0){
		cout << "create socket error" << endl;
		exit(SOCKET_ERR);
	}

	// 明确server是谁?
	struct sockaddr_in server;
	memset(&server, 0, sizeof server);

	server.sin_family = AF_INET; // 网络通信
	server.sin_addr.s_addr = inet_addr(serverip.c_str()); // IP地址
	server.sin_port = htons(serverport); // 端口号

	while(true)
	{
		// 用户输入
		string message;
		cout << "please enter# ";
		cin >> message;

		// 发送消息
		sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        // 接收消息
		char buffer[1024];
		struct sockaddr_in temp;
		socklen_t len = sizeof(temp);

		int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
		if(n > 0)
		{
			buffer[n] = 0;
			// 收到回响消息
			cout << "server echo " << buffer << endl;
		}

	}

	return 0;
}

makefile

.PHONY:all
all:udp_client udp_server 

udp_client:udp_client.cc
	g++ -o $@ $^ -std=c++11
udp_server:udp_server.cc
	g++ -o $@ $^ -std=c++11
	
.PHONY:clean
clean:
	rm -f udp_client udp_server

在这里插入图片描述


2. bash服务器

server.hpp

#pragma once
#include "err.hpp"
#include <iostream>
#include <memory>
#include <string>
#include <functional>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

namespace ns_server
{
	const static uint16_t default_port = 8080; 
	using func_t = function<string(string)>;

	class UdpServer
	{
	public:
		UdpServer(func_t func, uint16_t port = default_port)
			:func_(func)
			,port_(port)
		{}

		void InitServer()
		{
			// 创建套接字接口, 打开文件
			sock_ = socket(AF_INET, SOCK_DGRAM, 0);
			if(sock_ < 0)
			{
				cerr << "create socket error: " << strerror(errno) << endl;
				exit(SOCKET_ERR);
			}
			cout << "create socket success: " << sock_ << endl;

			// 给服务器指明IP地址和端口号
			struct sockaddr_in local;
			bzero(&local, sizeof local);
			local.sin_family = AF_INET; // 将16位地址类型设置为网络通信
			local.sin_port = htons(port_); // 将主机的端口号转为网络序列

			// local.sin_addr.s_addr = inet_addr(ip_.c_str());
			local.sin_addr.s_addr = INADDR_ANY;
			
			// 将套接字字段和文件字段进行绑定关联
			if(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				std::cerr << "bind socket error: " << strerror(errno) << std::endl;
				exit(BIND_ERR);
			}
			std::cout << "bind socket success: " << sock_ << std::endl;

		}

		void Start()
		{
			char buffer[1024];
			while(true)
			{
				// 获取用户数据报
				struct sockaddr_in peer;
				socklen_t len = sizeof(peer);
				int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
				if(n > 0)
					buffer[n] = '\0';
				else
					continue;
				// 将4字节的IP转换成字符串IP
				string clientip = inet_ntoa(peer.sin_addr); // 客户端IP

				// 将网络序列转换为主机序列
				uint16_t clientport = ntohs(peer.sin_port); // 客户端端口号

				cout << clientip << "-" << clientport << "-get message " << buffer << endl;

				// 做业务处理
				string message = func_(buffer);

				// 将消息发回客户端
				sendto(sock_, message.c_str(), message.size(), 0, (struct sockaddr*)&peer, sizeof(peer));

			}
		}


		~UdpServer()
		{}
	private:
		int sock_; // 套接字文件描述符
		uint16_t port_; // 端口号
		func_t func_;
	};
}

server.cc

#include "udp_server.hpp"

using namespace ns_server;

static void usage(string proc)
{
	cout << "usage:\t" << proc << "port\n" << endl;
}

// 安全检查函数
bool isPass(const string& command)
{
	bool pass = true;
	auto pos = command.find("rm");
    if(pos != string::npos) pass=false;
    pos = command.find("mv");
    if(pos != string::npos) pass=false;
    pos = command.find("while");
    if(pos != string::npos) pass=false;
    pos = command.find("kill");
    if(pos != string::npos) pass=false;
    return pass;
}

// 在你的在自己本地把命令给server,server再把执行结果给你!
string excuteCommand(string command)
{
	// 1. 安全检查
	if(!isPass(command))
		return "you are bad man!";

	// 2. 业务逻辑处理
	FILE* fp = popen(command.c_str(), "r");
	if(fp == nullptr)
		return "None";

	// 3. 获取结果
	char line[1024];
	string result;
	while(fgets(line, sizeof(line), fp) != nullptr)
	{
		result += line;
	}

	pclose(fp);
	return result;
}

int main(int argc, char* argv[]) // 命令行参数
{
	if(argc != 2)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}

	// 端口号
	uint16_t port = atoi(argv[1]);
	unique_ptr<UdpServer> usvr(new UdpServer(excuteCommand, port));

	usvr->InitServer();
	usvr->Start();

	return 0;
}

client.cc

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
using namespace std;

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; 
}

int main(int argc, char* argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}
	string serverip = argv[1];
	uint16_t serverport = atoi(argv[2]);

	int sock = socket(AF_INET, SOCK_DGRAM, 0);
	if(sock < 0){
		cout << "create socket error" << endl;
		exit(SOCKET_ERR);
	}

	// 明确server是谁?
	struct sockaddr_in server;
	memset(&server, 0, sizeof server);

	server.sin_family = AF_INET; // 网络通信
	server.sin_addr.s_addr = inet_addr(serverip.c_str()); // IP地址
	server.sin_port = htons(serverport); // 端口号

	while(true)
	{
		// 用户输入
		string message;
		cout << "please enter# ";
		// cin >> message;
		getline(cin, message);

		// 发送消息
		sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        // 接收消息
		char buffer[1024];
		struct sockaddr_in temp;
		socklen_t len = sizeof(temp);

		int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
		if(n > 0)
		{
			buffer[n] = 0;
			// 收到回响消息
			cout << "server echo " << buffer << endl;
		}

	}

	return 0;
}

err.hpp

#pragma once

enum
{
	USAGE_ERR = 1,
	SOCKET_ERR,
	BIND_ERR
};

在这里插入图片描述


3. 简易的公共聊天室

udp_server.hpp

#pragma once

#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unordered_map>
#include "err.hpp"
#include "RingQueue.hpp"
#include "lockGuard.hpp"
#include "Thread.hpp"

namespace ns_server
{
    const static uint16_t default_port = 8080;
    using func_t = std::function<std::string(std::string)>;

    class UdpServer
    {
    public:
        UdpServer(uint16_t port = default_port) : port_(port)
        {
            std::cout << "server addr: " << port_ << std::endl;
            pthread_mutex_init(&_lock, nullptr);

            _p = new Thread(1, std::bind(&UdpServer::Recv, this));
            _c = new Thread(2, std::bind(&UdpServer::Broadcast, this));
        }
        void Start()
        {
            // 1. 创建socket接口,打开网络文件
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
            if (sock_ < 0)
            {
                std::cerr << "create socket error: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "create socket success: " << sock_ << std::endl; // 3

            // 2. 给服务器指明IP地址(??)和Port
            struct sockaddr_in local; // 这个 local 在哪里定义呢?用户空间的特定函数的栈帧上,不在内核中!
            bzero(&local, sizeof(local));

            local.sin_family = AF_INET; // PF_INET
            local.sin_port = htons(port_);
            // inet_addr: 1,2
            // 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
            // 2. 需要将主机序列转化成为网络序列
            // 3. 云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,bind本主机上的任意IP
            if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                std::cerr << "bind socket error: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind socket success: " << sock_ << std::endl; // 3

            _p->run();
            _c->run();
        }

        void addUser(const std::string &name, const struct sockaddr_in &peer)
        {
            // onlineuserp[name] = peer;
            LockGuard lockguard(&_lock);
            auto iter = _onlineuser.find(name);
            if (iter != _onlineuser.end())
                return;
            _onlineuser.insert({name, peer});
        }
        void Recv()
        {
            char buffer[1024];
            while (true)
            {
                // 收
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); // 这里一定要写清楚,未来你传入的缓冲区大小
                int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                if (n > 0)
                    buffer[n] = '\0';
                else
                    continue;

                std::cout << "recv done ..." << std::endl;

                // 提取client信息 -- debug
                std::string clientip = inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port);
                std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;

                // 构建一个用户,并检查
                std::string name = clientip;
                name += "-";
                name += std::to_string(clientport);
                // 如果不存在,就插入,如果存在,什么都不做
                addUser(name, peer);

                std::string message = name + ">> " + buffer;

                _rq.push(message);
            }
        }
        void Broadcast()
        {
            while (true)
            {
                std::string sendstring;
                _rq.pop(&sendstring);

                std::vector<struct sockaddr_in> v;
                {
                    LockGuard lockguard(&_lock); 
                    for (auto& user : _onlineuser)
                    {
                        v.push_back(user.second);
                    }
                }
                for (auto& user : v)
                {	
					sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&(user), sizeof(user));
                    std::cout << "send done ... " << ntohs(user.sin_port) << std::endl;
                }
            }
        }
        ~UdpServer()
        {
            pthread_mutex_destroy(&_lock);
            _c->join();
            _p->join();

            delete _c;
            delete _p;
        }

    private:
        int sock_;
        uint16_t port_;
        std::unordered_map<std::string, struct sockaddr_in> _onlineuser;
        pthread_mutex_t _lock;
        RingQueue<std::string> _rq;
        Thread *_c;
        Thread *_p;
    };
}

udp_server.cc

#include "udp_server.hpp"
#include <memory>
#include <string>
#include <cstdio>

using namespace ns_server;
using namespace std;

static void usage(string proc)
{
    std::cout << "Usage:\n\t" << proc << " port\n"
              << std::endl;
}

// ./udp_server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<UdpServer> usvr(new UdpServer(port));

    usvr->Start();

    return 0;
}

udp_client.cc

#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
#include <cstring>
#include <pthread.h>

// 127.0.0.1: 本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

void *recver(void *args)
{
    int sock = *(static_cast<int *>(args));
	pthread_detach(pthread_self());
    while (true)
    {
        // 接受
        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl; //1
        }
    }
}

// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKET_ERR);
    }
    // client 这里要不要bind呢?要的!socket通信的本质[clientip:clientport, serverip:serverport]
    // 要不要自己bind呢?不需要自己bind,也不要自己bind,OS自动给我们进行bind -- 为什么?client的port要随机让OS分配防止client出现
    // 启动冲突 -- server 为什么要自己bind?1. server的端口不能随意改变,众所周知且不能随意改变的 2. 同一家公司的port号
    // 需要统一规范化

    // 明确server是谁
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    pthread_t tid;
    pthread_create(&tid, nullptr, recver, &sock);

    while (true)
    {
        // 多线程化??

        // 用户输入
        std::string message;
        std::cerr << "Please Enter Your Message# "; // 2
        // std::cin >> message;

        std::getline(std::cin, message);
        // 什么时候bind的?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文
        // 发送
		
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }


    return 0;
}

在这里插入图片描述


五、套接字相关知识梳理

套接字创建

int socket(int domain, int type, int protocol);
  • domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
  • type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
  • protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。

当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的 网络文件”,最后3号文件描述符作为socket函数的返回值返回给了用户。

返回值说明:

套接字创建成功返回一个文件描述符,创建失败返回-1, 同时错误码被设置

注意:

对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。


服务端绑定

当套接字创建成功了之后,作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来

bind函数

int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
  • sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
  • addr网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:绑定成功返回0,绑定失败会返回-1,同时错误码被设置。

struct sockaddr_in结构体

在绑定的时候需要将网络相关的信息填充到这个结构体当中,然后将该结构体作为bind函数的第二个参数进行传入,这个结构体就是struct sockaddr_in

struct sockaddr_in 当中的成员如下:

  • sin_family:表示协议家族。
  • sin_port:表示端口号,是一个16位的整数。
  • sin_addr:表示IP地址,是一个32位的整数

在进行绑定的时候需要将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来。

在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的 htons函数将端口号转为网络序列。此外,由于网络当中传输的是整数IP,我们需要调用 inet_addr函数将字符串IP转换成整数IP,同时将主机序列转化成为网络序列,然后再将转换后的整数IP进行设置。

字符串IP转整数IP的函数:inet_addr函数

in_addr_t inet_addr(const char *cp);

整数IP转换为字符串IP的函数(不会将网络序列转换为主机序列):inet_ntoa函数

char *inet_ntoa(struct in_addr in);

需要注意的是,传入inet_ntoa函数的参数类型是in_addr,因此我们在传参时不需要选中in_addr结构当中的32位的成员传入,直接传入in_addr结构体即可。


发送和接收数据

发送数据函数:sendto函数

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
  • buf:待写入数据的存放位置。
  • len:期望写入数据的字节数。
  • flags:写入的方式。一般设置为0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。 addrlen:传入dest_addr结构体的长度。
  • 返回值说明:
    • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

注意:

由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。由于sendto函数提供的参数也是struct sockaddr* 类型的,因此我们在传入结构体地址时需要将struct sockaddr_in* 类型进行强转。

读取数据函数:recvfrom函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
  • buf:读取数据的存放位置。
  • len:期望读取数据的字节数。
  • flags:读取的方式。一般设置为0,表示阻塞读取。
  • src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
  • 返回值说明:
    • 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置

回环地址

IP地址 “127.0.0.1” 是一个特殊的IPv4地址,被称为回环地址(Loopback Address)。它用于在同一台计算机上进行网络通信的特殊目的。

当计算机上的应用程序尝试连接到 “127.0.0.1” 或发送数据到该地址时,数据包将被发送到计算机上的网络协议栈中,并立即被返回到发送方。这样,应用程序可以通过回环地址与自身进行通信,而无需与外部网络进行实际的网络连接。

这里我们可以使用 netstat 命令查看网络信息。


INADDR_ANY

由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。

绑定INADDR_ANY的好处:

当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,但一台服务器上端口号为8081的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为8081的服务。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。而如果服务端绑定的是INADDR_ANY,那么只要是发送给端口号为8081的服务的数据,系统都会可以将数据自底向上交给该服务端。



http://www.niftyadmin.cn/n/5054411.html

相关文章

云原生应用开发:从构建到部署的全过程指南

文章目录 第1节&#xff1a;理解云原生应用1.1 什么是云原生应用&#xff1f;1.2 为什么选择云原生应用&#xff1f; 第2节&#xff1a;构建云原生应用2.1 选择合适的编程语言和框架2.2 使用容器化2.3 微服务架构2.4 自动化构建和测试 第3节&#xff1a;部署云原生应用3.1 选择…

【JAVA】读取classpath下的文件

在Java中从classpath读取文件,可以使用下面的方法: 使用ClassLoader获取classpath资源作为流读取: InputStream input this.getClass().getClassLoader().getResourceAsStream("config.properties");使用Class的getResourceAsStream方法读取classpath资源: Input…

软件测试-Web自动化测试

Web自动化测试 1.什么是自动化测试 自动测试指软件测试的自动化&#xff0c;根据预设的要求&#xff0c;采用一些工具来帮我们完成一些频繁操作的事情&#xff0c;然后评估运行结果。随着软件的更新迭代版本越来越多&#xff0c;仅仅通过人工测试回归所有的版本肯定是不现实的…

【Verilog教程】6.7 Verilog流水线

关键词&#xff1a;流水线&#xff0c;乘法器 硬件描述语言的一个突出优点就是指令执行的并行性。多条语句能够在相同时钟周期内并行处理多个信号数据。 但是当数据串行输入时&#xff0c;指令执行的并行性并不能体现出其优势。而且很多时候有些计算并不能在一个或两个时钟周期…

ElasticSearch - 基于 拼音分词器 和 IK分词器 模拟实现“百度”搜索框自动补全功能

目录 一、自动补全 1.1、效果说明 1.2、安装拼音分词器 1.3、自定义分词器 1.3.1、为什么要自定义分词器 1.3.2、分词器的构成 1.3.3、自定义分词器 1.3.4、面临的问题和解决办法 问题 解决方案 1.4、completion suggester 查询 1.4.1、基本概念和语法 1.4.2、示例…

对象存储,从单机到分布式的演进

关于数据存储的相关知识,请大家关注“数据存储张”,各大平台同名。 通过《什么是云存储?从对象存储说起》我们对对象存储的历史、概念和基本使用有了一个大概的认识。而且我们以Minio为例,通过单机部署的模式实际操作了一下对象存储的GUI,感受了一下对象存储的用法。 在上…

【三相有源电力滤波器】使用同步参考系控制的三相有源功率滤波器(Simulink)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Java 消息策略的实现 - Kafak 是怎么设计的

这个也是开放讨论题&#xff0c;主要讨论下 Kafka 在消息中是如何进行实现的。 1_cCyPNzf95ygMFUgsrleHtw976506 21.4 KB 总结 这个题目的开发性太强了。 Kafka 可以用的地方非常多&#xff0c;我经历过的项目有 Kafka 用在消息处理策略上的。这个主要是 IoT 项目&#xff0c…