TCP/IP网络编程(6)

2023-11-17

1. IO复用

并发服务器的实现方法

在网络程序中,数据通信时间比CPU运算时间占比更大,因此,采用并发的形式向多个客户端提供服务是一种有效利用CPU的方式。并发服务器的主要实现模型及方法如下所示:

  • 多进程服务器,通过常见多个进程提供服务
  • 多路复用服务器,通过捆绑并统一管理IO对象提供服务
  • 多线程服务器,通过生成与客户端等量的线程提供服务

基于IO复用的服务器端:

复用的概念:在一个通信频道中传输多个数据(信号)的技术。典型的例子:通信领域时分复用技术以及频分复用技术,在同一条通信线路上,分时传递不同的信号,提高信号的线路的利用率,或者是叠加不同频率的信号。

select函数实现并发服务器

select函数的功能和调用顺序:使用select函数可以将多个文件描述符集中到一起统一监视:

  • 是否存在套接字接收数据
  • 无需阻塞传输数据的套接字有哪些
  • 哪些套接字发生了异常

1. 设置文件描述符

利用select函数可以监视多个文件描述符(套接字),此时需要将文件描述符集中到一起,集中时需要按照监视项(接收,传输,异常)进行区分,可使用fd_set数组执行此操作。fd_set是存有0和1的位数组。

fd_i表示文件描述符i所在的位置,如果对应的位置的值为1,则表示该文件描述符是监视对象,如上图中的文件描述符fd0和fd2就是监视对象。由于fd_set是位数组,直接将文件描述符的数字值注册到fd_set数组中较为繁琐,因此可使用已有的宏定义进行注册等一系列操作:

FD_ZERO(fd_set* fdset);           // 将位数组fdset的所有位初始化为0
FD_SET(int fd, fd_set* fdset);    // 注册文件描述符fd
FD_CLR(int fd, fd_set* fdset);    // 清楚文件描述符fd的信息
FD_ISSET(int fd, fd_set* fdset);  // 若位数组fdset中包含文件描述符的信息,则返回真

上述函数可以用于验证select函数的调用结果:

2. 调用select函数

#include <sys/select.h>
#include <sys/time.h>

/*
   select函数
   input:
   int maxfd: 监视对象文件描述符数量
   fd_set* readset: 用于注册 读取数据的 文件描述符对象
   fd_set* writeset:用于注册 传输无阻塞数据的 文件描述符对象
   fd_set* exceptset:若需要关注文件描述符是否发生异常,则将文件描述符注册到此数组中
   const struct timeval *timeout:用于设置select函数的超时时间

   return:
   -1 : select函数发生错误
   0  :select函数超时
   >0 : 发生事件的文件描述符数量 (可读,可写,异常)
*/
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval *timeout);

2.1 文件描述符的监视范围:

文件描述符的监视范围与select函数的第一个参数有关,select函数要求通过第一个参数传递监视对象文件描述符的数量,因此,需要得到注册在fd_set变量中的文件描述符的数量。每次新建文件描述符的时候,其值都会加1,因此仅仅需要将最大的文件描述符的值加1,传递给select函数的第一个参数即可。(加1是因为文件描述符从0开始)

2.2 select函数的超时时间:

select函数的超时时间与select函数的最后一个参数有关,本来select函数只有在监视的文件描述符发生变化的时候才会返回,否则就会一直处于阻塞状态,如果设置了超时时间,则select函数在超过超时时间之后,如果未发生变化,也会返回,且返回值位0。如果不想设置超时时间,则最后一个参数传递NULL即可。

2.2 调用select函数后fd_set的变化:

 select函数调用完成后,向其传递的fd_set数组将可能发生变化,原来所有为1的位置均会变为0,但是如果文件描述符发生了变化,则其对应位置的值仍然为1,因此可以认为值为1的位置上的文件描述符发生了变化。

3.基于windows平台的select函数

windows平台上的select函数与Linux下的基本一致,区别是,select函数的第一个参数是为了与Linux平台保持兼容性二添加的,并没有特殊的含义。

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfsd, const struct timeval* timeout);


// timeval定义
typedef struct timeval
{
    long tv_sec;      // seconds
    long tv_usec;     // microseconds
}

windows中fd_set不像Linux中那样采用位数组,而实定义了一个结构体:

typedef struct fd_set
{
    u_int fd_count;
    SOCKET fd_array[FD_SIZE];
}

其中,fd_count用于保存套接字句柄数量,fd_array用于保存套接字句柄。Window下这样定义的原因:Linux文件描述符从0开始递增,因此可以找出当前的文件描述符的数量和最后生成的文件描述符的关系。但是Windows套接字句柄并非从0开始,而且句柄的数值之间无规律可循,因此需要保存句柄数量以及句柄数组。但是处理fd_set的四个宏定义的名称,方法,以及功能与Linux完全相同,完全具备与Linux的兼容性。

4. select函数的应用示例(回声服务器)

服务端:

// echoserver.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>

#pragma comment(lib, "Ws2_32.lib")

// 宏定义
#define BUFF_SIZE    1*1024      // 缓冲区大小
#define ADDR_SIZE    sizeof(SOCKADDR_IN)
#define SOCK_PORT    13000

// error handler
void error_handle(char* message)
{
	printf("%s\n", message);
	system("pause");
	exit(1);
}


int main()
{
	WSADATA wsadata;
	SOCKET hServerSocket;       // 服务端socket监听
	SOCKET hClientSocket;

	// 地址
	SOCKADDR_IN serverAddr;
	SOCKADDR_IN clientAddr;

	TIMEVAL timeout;    // 超时时间

	fd_set reads, copyReads;

	char buffer[BUFF_SIZE];     // 定义缓冲区

	int addr_szie = sizeof(SOCKADDR_IN);

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
	{
		error_handle("WsaStratUp() Failed");
	}

	// 初始化服务端socket
	hServerSocket = socket(PF_INET, SOCK_STREAM, 0);
	// 初始化服务端地址
	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(SOCK_PORT);

	if (bind(hServerSocket, (SOCKADDR*)&serverAddr, ADDR_SIZE) == SOCKET_ERROR)
	{
		error_handle("Failed to bind the server socket.");
	}

	// 开始监听
	if (listen(hServerSocket, 5) == SOCKET_ERROR)
	{
		error_handle("Failed to start the server socket listen");
	}

	FD_ZERO(&reads);
	FD_SET(hServerSocket, &reads);

	printf("Waiting for connect from client...\n");

	while (true)
	{
		copyReads = reads;

		// 设置超时时间   每次select之后,timeout中的值将被替换为超时前剩余的时间,因此在每一次select之前都需要重新设置超时时间
		timeout.tv_sec = 5;
		timeout.tv_usec = 5000;

		int res = select(0, &copyReads, NULL, NULL, &timeout);

		if (res < 0)
		{
			// 发生异常  清除socket,处理异常并退出
			closesocket(hServerSocket);
			WSACleanup();
			error_handle("Error occurs when slect.");
		}
		else if (res == 0)
		{
			// 返回值为0表示超时
			printf("The select timeout.\n");
			continue;
		}
		else
		{
			for (int i=0; i<reads.fd_count; i++)
			{
				// 在select之后,fd_set中发生变化的套接字句柄所在位置保持为1,未发生变化的全部为0
				if (FD_ISSET(reads.fd_array[i], &copyReads))    // 有套接字发生变化 
				{
					if (reads.fd_array[i] == hServerSocket)     // 如果是服务端监听socket发生变化
					{
						// 接收新的连接
						hClientSocket = accept(hServerSocket, (SOCKADDR*)&clientAddr, &addr_szie);

						// 将对应的客户端socket加入到fd_set中
						FD_SET(hClientSocket, &reads);

						printf("New connect client address:%s , port:%d.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
					}
					else
					{
						// 客户端套接字发生变化,接收数据
						hClientSocket = reads.fd_array[i];

						memset(buffer, 0, BUFF_SIZE);

						int strLen = recv(hClientSocket, buffer, BUFF_SIZE, 0);

						if (strLen <= 0)
						{
							// close the request
							FD_CLR(hClientSocket, &reads);
							closesocket(hClientSocket);
							printf("Closed socket %d\n", hClientSocket);
						}
						else
						{
							// 将数据发送回客户端
							send(hClientSocket, buffer, strLen, 0);
						}
					}
				}
			}
		}
	}

	closesocket(hServerSocket);
	WSACleanup();
    return 0;
}

客户端测试程序:
 

// clentdemon.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>

#pragma comment(lib, "Ws2_32.lib")

// 宏定义
#define BUFF_SIZE   1*1024
#define ADDR_SIZE   sizeof(SOCKADDR_IN)
#define SERV_ADDR   "127.0.0.1"       // 服务端地址
#define SOCK_PORT   13000             // 端口

// error handler
void error_handle(char* message)
{
	printf("%s\n", message);
	system("pause");
	exit(1);
}


int main()
{
	char buffer[BUFF_SIZE];     // 缓冲区

	WSADATA wsaData;

	SOCKET hServerSocket;        // 服务端socket
	SOCKADDR_IN serverAddr;      // 服务端地址

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		error_handle("Failed when wsaStartup!");
	}

	memset(buffer, 0, BUFF_SIZE);

	memset(&serverAddr, 0, ADDR_SIZE);
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = inet_addr(SERV_ADDR);
	serverAddr.sin_port = htons(SOCK_PORT);

	hServerSocket = socket(PF_INET, SOCK_STREAM, 0);

	// 连接客户端
	if (connect(hServerSocket, (SOCKADDR*)&serverAddr, ADDR_SIZE) == SOCKET_ERROR)
	{
		error_handle("Failed to connect to server.");
	}

	printf("Successfully connect to server!\n");

	char* quitFlag1 = "q";
	char* quitFlag2 = "Q";

	while (true)
	{
		memset(buffer, 0, BUFF_SIZE);
		printf("Please input the message to send: ");
		scanf("%s", buffer);

		if (strlen(buffer) > 0)
		{
			if (strncmp(buffer, quitFlag1, 1)==0 || strncmp(buffer, quitFlag2, 1)==0)
			{
				printf("Quit the client routine.\n");
				break;
			}
			else
			{
				send(hServerSocket, buffer, BUFF_SIZE, 0);      // 发送消息
			}
		}

		memset(buffer, 0, BUFF_SIZE);

		int strLen = recv(hServerSocket, buffer, BUFF_SIZE, 0);

		if (strLen > 0)
		{
			printf("Receive from server: %s\n", buffer);
		}
		else 
			continue;
	}

	closesocket(hServerSocket);
	WSACleanup();

    return 0;
}

测试效果:

服务端效果:

客户端测试效果:

写一个多线程的客户端测试程序(使用c++11标准库多线程实现)

// clentdemon.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>

using namespace std;

#pragma comment(lib, "Ws2_32.lib")

// 宏定义
#define BUFF_SIZE   1*1024
#define ADDR_SIZE   sizeof(SOCKADDR_IN)
#define SERV_ADDR   "127.0.0.1"       // 服务端地址
#define SOCK_PORT   13000             // 端口

// error handler
void error_handle(char* message)
{
	printf("%s\n", message);
	system("pause");
	exit(1);
}

class client
{
public:
	client(int threadNum)
	{
		m_threadNum = threadNum;

		init();
	}

	~client()
	{
		for (int i=0; i<m_threadArray.size(); i++)
		{
			thread* pThread = m_threadArray.at(i);

			if (pThread)
			{
				delete pThread;
				pThread = NULL;
			}
		}

		m_threadArray.clear();
	}

	void exec()
	{
		for (int i=0; i<m_threadNum; ++i)
		{
			thread* pThread = m_threadArray.at(i);

			/*
			joinable()函数是一个布尔类型的函数,他会返回一个布尔值来表示当前的线程是否是可执行线程(能被join或者detach),因为相同的线程不能join两次,
			也不能join完再detach,同理也不能detach完了再去join,所以joinable函数就是用来判断当前这个线程是否可以joinable的。通常不能被joinable有
			以下几种情况:

			1)由thread的缺省构造函数而造成的(thread()没有参数)。

			2)该thread被move过(包括move构造和move赋值)。

			3)该线程被join或者detach过。

			*/
			if (!pThread || !pThread->joinable())
			{
				printf("Failed to start thread %d.\n", this_thread::get_id());
				continue;
			}

			pThread->detach();

			printf("Thread %d has been started!\n", this_thread::get_id());
			/*
			join() 表示主线程需要等待子线程运行结束回收掉子线程的资源后,再往下运行
			detach() 表示子线程不需要等待主线程,detach的作用就是将主线程与子线程分离,
			主线程将不再等待子线程的运行,也就是说两个线程同时运行,当主线程结束的时候,
			进程结束,此时子线程也会被回收。
			*/
		}
	}

	bool isAllThreadFinished()
	{
		return (m_threadNum == m_finishedThreadCnt);
	}

protected:

	void init()
	{
		m_finishedThreadCnt = 0;

		// 初始化所有线程
		for (int i=0; i < m_threadNum; ++i)
		{
			thread* pThread = new thread(&client::funct, this);     // 注意类成员作为thread函数的时候的特殊写法
			m_threadArray.push_back(pThread);
		}
	}

	void funct()
	{
		char buffer[BUFF_SIZE];     // 缓冲区

		WSADATA wsaData;

		SOCKET hServerSocket;        // 服务端socket
		SOCKADDR_IN serverAddr;      // 服务端地址

		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		{
			printf("Failed when wsaStartup in thread %d!", this_thread::get_id());

			//线程结束标记
			m_threadCntMutex.lock();
			m_finishedThreadCnt++;
			m_threadCntMutex.unlock();

			return;
		}

		memset(buffer, 0, BUFF_SIZE);

		memset(&serverAddr, 0, ADDR_SIZE);
		serverAddr.sin_family = AF_INET;
		serverAddr.sin_addr.s_addr = inet_addr(SERV_ADDR);
		serverAddr.sin_port = htons(SOCK_PORT);

		hServerSocket = socket(PF_INET, SOCK_STREAM, 0);

		// 连接客户端
		if (connect(hServerSocket, (SOCKADDR*)&serverAddr, ADDR_SIZE) == SOCKET_ERROR)
		{
			// error_handle("Failed to connect to server.");
			printf("Thread %d Failed to connect to the server.\n", this_thread::get_id());
			closesocket(hServerSocket);
			WSACleanup();

			//线程结束标记
			m_threadCntMutex.lock();
			m_finishedThreadCnt++;
			m_threadCntMutex.unlock();

			return;
		}

		printf("Socket in thread %d successfully connected to server!\n", this_thread::get_id());

		char* quitFlag1 = "q";
		char* quitFlag2 = "Q";

		int count = 0;

		while (true)
		{
			chrono::seconds sec(2);

			this_thread::sleep_for(sec);

			count++;

			sprintf(buffer, "Hello world info from thread %d.", this_thread::get_id());

			if (count > 5)
			{
				printf("The socket in thread %d will exit, bye bye.\n", this_thread::get_id());
				break;
			}

			if (strlen(buffer) > 0)
			{
				if (strncmp(buffer, quitFlag1, 1) == 0 || strncmp(buffer, quitFlag2, 1) == 0)
				{
					printf("Quit the client routine.\n");
					break;
				}
				else
				{
					send(hServerSocket, buffer, BUFF_SIZE, 0);      // 发送消息
				}
			}

			memset(buffer, 0, BUFF_SIZE);

			int strLen = recv(hServerSocket, buffer, BUFF_SIZE, 0);

			if (strLen > 0)
			{
				printf("Receive from server: %s\n", buffer);
			}
			else
				continue;
		}

		closesocket(hServerSocket);
		WSACleanup();
		printf("Thread %d successfully exit.\n", this_thread::get_id());

		//线程结束标记
		m_threadCntMutex.lock();
		m_finishedThreadCnt++;
		m_threadCntMutex.unlock();

		return;
	}

private:
	int m_threadNum;
	int m_finishedThreadCnt;
	mutex m_threadCntMutex;
	vector<thread*> m_threadArray;
};


int main()
{
	client c(5);    // 客户端创建5个线程

	c.exec();

	/*
	这里必须要判断所有线程是否都已经结束,如果不判断,会导致socket线程还在运行,
	但是主线程已经结束,主线程结束,detach或者join的所有子线程也会被回收,导致
	线程运行结果不正确。
	*/
	while (!c.isAllThreadFinished());        // 等待所有线程结束

    return 0;
}

多线程运行结果:

服务端:

 多线程客户端运行结果

​​​​​​​
 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

TCP/IP网络编程(6) 的相关文章

随机推荐

  • vue3项目打包后整合到springboot项目中运行

    概述 一般来说 前后端分离项目 比如vue3 springboot的前后端分离项目 一般把vue3项目打包后部署到nginx或者tomcat上面 springboot项目单独打包 那如果想把vue3项目打包后直接部署到springboot项
  • 对矩阵的处理 MATLAB

    矩阵 1 创建矩阵 1 直接输入法s 1 1 2 2 3 3 4 4 结果如图 2 利用某些函数zeros或ones创建 3 复数矩阵 直接按照直接输入法来建立矩阵 但是元素可以直接打成复数的形式 a bj 2 还有就是分别建立一个实部还有
  • Android mob(sharesdk)微信分享/微信朋友圈分享/QQ分享/QQ空间分享/新浪微博分享(自定义)

    使用场景 H5混合开发转Android 原生开发 之前也是用的友盟分享 微信原生 QQ原生 微博原生分享 今天这个项目恰好是用的mob的sharesdk分享 老实说 还是第一次使用 一开始接手项目的时候 还以为mob是个后台 来一张效果图
  • pandorabox php7,新路由3newifi D2专用潘多拉PandoraBox固件SFE快速转发超强信号不掉速eeprom...

    今天就分享一个这次给新路由3 newifi3 PandoraBox 潘多拉固件下载刷的第三方固件潘多拉PandoraBox固件 PandoraBox是什么 PandoraBox 是基于LEDE OpenWrt框架高度定制的中文本地化固件 应
  • HTML语言中vw的用法,CSS中如何使用视窗单位

    视窗 Viewport 单位已经有了好几年了 但我们并没有看到它被经常使用 它们现在正在被所有的主流浏览器所支持并提供独特的功能使它在特定情况下非常有用 特别是那些涉及响应式的设计 介绍视窗 Viewport 单位 视窗 Viewport
  • arouter 自定生成autowired

    原文地址 Evil Mouth s Blog ARouter Autowired 自动注入 May 31 2020 前言 ARouter 有一个 Autowired 的注解 能自动帮我们赋值一些变量 例如 public class Main
  • HBase 维护--查看HLog和HFile

    查看HLog 看了一些文章 HBase高可靠性是实现了HLog Write ahead Log 机制 那么HLog到底存在哪里了呢 首先去HDFS的 hbase目录查看一下 hadoop fs ls R hbase 可以看到hbase下面有
  • MariaDB数据库服务器

    目录 一 什么是数据库 二 什么是关系型数据库 三 数据库字符集和排序规则是什么 四 常用数据类型 五 Mariadb数据库相关配置案例 一 什么是数据库 数据库 DB 是以一定方式长期存储在计算机硬盘内 能与多个用户共享 具有尽可能小的冗
  • android 自动获取短信,安卓app怎样获取短信验证码自动输入

    这个你要自己写吗 我建议你直接调用短信平台的接口不就可以了吗 短信发送 接口地址 String url http 183 203 28 5 9000 HttpSmsMt 下发时间 String mttime new SimpleDateFo
  • [翻译] ProtoBuf 官方文档(全)

    ProtoBuf CSDN搜索 https so csdn net so search q ProtoBuf t blog u chuifuhuo6864
  • nginx重启命令

    nginx s reload 修改配置后重新加载生效 nginx s reopen 重新打开日志文件 nginx t c path to nginx conf 测试nginx配置文件是否正确 关闭nginx nginx s stop 快速停
  • 解决在Anaconda下安装tensorflow报错的问题 ModuleNotFoundError: No module named ‘tensorflow‘

    解决在Anaconda下安装tensorflow报错的问题 Traceback most recent call last File line 1 in ModuleNotFoundError No module named tensorf
  • 宽字节注入入门详解

    原理 GBK 占用两字节 ASCII占用一字节 PHP中编码为GBK 函数执行添加的是ASCII编码 添加的符号为 MYSQL默认字符集是GBK等宽字节字符集 大家都知道 df 被PHP转义 开启GPC 用addslashes函数 或者ic
  • 第二章-注入漏洞

    第二章 注入漏洞 第一节 SQL注入原理 1 1 SQL注入的原因 语言分类 解释型语言和编译型语言 解释型语言是一种在运行时由一个运行时组件解释语言代码并执行其中包含的指令的语言 而编译型语言是代码在生成时转换为机器指令 然后在运行时直接
  • uniapp弹幕滚动到底部

    发布的弹幕至于最底部
  • 【linux】linux 离线安装 curl命令

    文章目录 1 概述 2 curl安装步骤 3 验证 原创不易 且行且珍惜 1 概述 最近在忙一个艰苦的环境 没有yarn界面 没有flink界面 没有es界面 没有kibana界面 条件艰苦 且行且艰险 这个环境发现es日志不入库 然后查看
  • 内网渗透工具-反向代理FRP

    内网渗透工具 反向代理FRP 0x1 简介 FRP是一个比较流行而且成熟的内网渗透工具 支持 TCP UDP HTTP HTTPS 等多种协议 0x2 前期准备 工具准备 可在官方github仓库下载 https github com fa
  • ‘mvn‘不是内部或外部命令

    解决方案有两种 一 1 如果没有安装maven 在IDEA中使用maven 提示mvn不是内部命令 需要在环境变量中的用户变量的Path中添加maven的bin路径 重启下IDEA即可 1 环境变量 用户 2 Path 添加IDEA下的ma
  • Pytorch框架下训练网络的代码结构

    PyTorch 是一个基于 Torch 的 Python 开源机器学习库 用于自然语言处理等应用程序 它主要由 Facebook 的人工智能研究小组开发 PyTorch 提供两个高级功能 1 具有强大的 GPU 加速的张量计算 如 NumP
  • TCP/IP网络编程(6)

    1 IO复用 并发服务器的实现方法 在网络程序中 数据通信时间比CPU运算时间占比更大 因此 采用并发的形式向多个客户端提供服务是一种有效利用CPU的方式 并发服务器的主要实现模型及方法如下所示 多进程服务器 通过常见多个进程提供服务 多路