网络编程套接字(3)

网络编程套接字

  • 简单的TCP英译汉服务器
  • 地址转换函数
    • 字符串IP转整数IP
    • 整数IP转字符串IP
    • 关于inet_ntoa函数
    • 并发场景下的inet_ntoa函数
    • 绑定失败问题
    • TCP协议通讯流程
    • 数据传输的过程
    • 数据交互
    • 四次挥手的过程
    • 端口连接

简单的TCP英译汉服务器

之前我们是以回调的方式处理任务的,当线程池当中的线程从任务队列中拿出一个任务后,会调用该任务对应的Run方法处理该任务,而实际在这个Run方法当中会以仿函数的方式调用handler方法,因此我们只需更改Handler类当中对()的重载函数即可,而其他与通信相关的代码我们一律不用更改。

英译汉时需要根据英文单词找到其对应的中文意思,因此我们需要建立一张映射表,其中英文单词作为映射表中的键值key,而中文意思作为与键值相对应的value,这里可以直接使用C++STL容器当中的unordered_map容器。

class Handler
{
public:
	Handler()
	{}
	~Handler()
	{}
	void operator()(int sock, std::string client_ip, int client_port)
	{
		//only for test
		std::unordered_map<std::string, std::string> dict;
		dict.insert(std::make_pair("dragon", "龙"));
		dict.insert(std::make_pair("blog", "博客"));
		dict.insert(std::make_pair("socket", "套接字"));
		
		char buffer[1024];
		std::string value;
		while (true){
			ssize_t size = read(sock, buffer, sizeof(buffer)-1);
			if (size > 0){ //读取成功
				buffer[size] = '\0';
				std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;

				std::string key = buffer;
				auto it = dict.find(key);
				if (it != dict.end()){
					value = it->second;
				}
				else{
					value = key;
				}
				write(sock, value.c_str(), value.size());
			}
			else if (size == 0){ //对端关闭连接
				std::cout << client_ip << ":" << client_port << " close!" << std::endl;
				break;
			}
			else{ //读取失败
				std::cerr << sock << " read error!" << std::endl;
				break;
			}
		}
		close(sock); //归还文件描述符
		std::cout << client_ip << ":" << client_port << " service done!" << std::endl;
	}
};

说明一下:

这里只是测试更改后的服务器能否为客户端提供英译汉服务,因此直接将这张映射表定义在了()重载函数当中。
初始化这张映射表对应的操作,就是建立单词与其中文意思之间的映射关系,代码中为了测试只简单建立了三组映射关系。此外,初始化映射表的操作应该重新封装出一个初始化函数,否则每次为客户端提供英译汉服务时都会重新建立映射关系。
如果你自己要实现一个英译汉服务器,应该将这张映射表定义为静态,保证全局只有一张映射表,在启动服务器时就可以对这张映射表进行初始化。可以将中英文对照单独写在一个文件当中,在启动服务器时就可以将该文件加载进来,建立对应的映射关系。(这里我们只是做测试的,就不做过多的设计了)
当服务端在为客户端提供英译汉服务时,如果在映射表中不存在对应key值的键值对,则服务端会直接将用户发来的数据响应给客户端。

地址转换函数

字符串IP转整数IP

inet_ntoa函数
inet_ntoa函数的函数原型如下:

int inet_aton(const char *cp, struct in_addr *inp);

参数说明:

cp:待转换的字符串IP。
inp:转换后的整数IP,这是一个输出型参数。
返回值说明:

如果转换成功则返回一个非零值,如果输入的地址不正确则返回零值。
inet_addr函数
inet_addr函数的函数原型如下:

in_addr_t inet_addr(const char *cp);

参数说明:

cp:待转换的字符串IP。
返回值说明:

如果输入的地址有效,则返回转换后的整数IP;如果输入的地址无效,则返回INADDR_NONE(通常为-1)。
inet_pton函数
inet_pton函数的函数原型如下:

int inet_pton(int af, const char *src, void *dst);

参数说明:

af:协议家族。
src:待转换的字符串IP。
dst:转换后的整数IP,这是一个输出型参数。
返回值说明:

如果转换成功,则返回1。
如果输入的字符串IP无效,则返回0。
如果输入的协议家族af无效,则返回-1,并将errno设置为EAFNOSUPPORT。

整数IP转字符串IP

inet_ntoa函数
inet_ntoa函数的函数原型如下:

char *inet_ntoa(struct in_addr in);

参数说明:

in:待转换的整数IP。
返回值说明:

返回转换后的字符串IP。
inet_ntop函数
inet_ntop函数的函数原型如下:

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数说明:

af:协议家族。
src:待转换的整数IP。
dst:转换后的字符串IP,这是一个输出型参数。
size:用于指明dst中可用的字节数。
返回值说明:

如果转换成功,则返回一个指向dst的非空指针;如果转换失败,则返回NULL。
说明一下
我们最常用的两个转换函数是inet_addr和inet_ntoa,因为这两个函数足够简单。这两个函数的参数就是需要转换的字符串IP或整数IP,而这两个函数的返回值就是对应的整数IP和字符串IP。
其中inet_pton和inet_ntop函数不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此这两个函数中对应的参数类型是void*。
实际这些转换函数都是为了满足某些打印场景的,除此之外,更多的是用来做某些数据分析,比如网络安全方面的数据分析。

关于inet_ntoa函数

inet_ntoa函数可以将四字节的整数IP转换成字符串IP,其中该函数返回的这个转换后的字符串IP是存储在静态存储区的,不需要调用者手动进行释放,但如果我们多次调用inet_ntoa函数,此时就会出现数据覆盖的问题。

并发场景下的inet_ntoa函数

inet_ntoa函数内部只在静态存储区申请了一块区域,用于存储转换后的字符串IP,那么在线程场景下这块区域就叫做临界区,多线程在不加锁的情况下同时访问临界区必然会出现异常情况。并且在APUE中,也明确提出inet_ntoa不是线程安全的函数。

下面我们在多线程场景下对inet_ntoa函数进行测试:

#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>

void* Func1(void* arg)
{
	struct sockaddr_in* p = (struct sockaddr_in*)arg;
	while (1){
		char* ptr1 = inet_ntoa(p->sin_addr);
		std::cout << "ptr1: " << ptr1 << std::endl;
		sleep(1);
	}
}
void* Func2(void* arg)
{
	struct sockaddr_in* p = (struct sockaddr_in*)arg;
	while (1){
		char* ptr2 = inet_ntoa(p->sin_addr);
		std::cout << "ptr2: " << ptr2 << std::endl;
		sleep(1);
	}
}
int main()
{
	struct sockaddr_in addr1;
	struct sockaddr_in addr2;
	addr1.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;
	
	pthread_t tid1 = 0;
	pthread_create(&tid1, nullptr, Func1, &addr1);
	pthread_t tid2 = 0;
	pthread_create(&tid2, nullptr, Func2, &addr2);
	
	pthread_join(tid1, nullptr);
	pthread_join(tid2, nullptr);
	return 0;
}

绑定失败问题

资源未释放干净
当我们在测试网络代码时,先将服务端绑定8081端口运行,然后运行客户端,并让客户端连接当前服务器。
此时在有客户端连接服务端的情况下,如果直接将服务端关闭,此时服务端要想再次绑定8081号端口运行,就可能会绑定失败。
绑定失败这个问题涉及TCP通信中双方状态变化的一个细节,这里暂时无法解释清楚,后面博主在讲解TCP协议细节时会详谈。这里想说明的就是,绑定是有可能失败的,这里绑定失败实际是因为服务端退出时没有将资源释放干净。
端口号已被其他程序绑定
此外,绑定失败还有可能因为当前端口号已经被其他程序绑定了。比如一个程序已经绑定了8081号端口,此时另一个程序也想绑定8081号端口,此时该程序就会绑定失败。
这实际也就验证了一个端口号只能被一个进程所绑定这样的规则,此时也就确保了端口号到服务之间的映射本身就具备唯一性。
无法绑定的端口号
我们自己编写的服务器代码在绑定端口号时,尽量不要绑定1024以下的端口号。一般云服务器只能绑定1024及其往上的端口号,因为1024以下的端口已经约定俗成被其他一些比较成熟的服务所使用了,如果我们绑定1024以下的端口号,那么会绑定失败。
因此我们一般只能绑定1024及其往上的端口号,最好绑定8000及其网上的端口号。

TCP协议通讯流程

通讯流程总览
下图是基于TCP协议的客户端/服务器程序的一般流程:
在这里插入图片描述
下面我们结合TCP协议的通信流程,来初步认识一下三次握手和四次挥手,以及建立连接和断开连接与各个网络接口之间的对应关系。
在这里插入图片描述
初始化服务器
当服务器完成套接字创建、绑定以及监听的初始化动作之后,就可以调用accept函数阻塞等待客户端发起请求连接了。

服务器初始化:

调用socket,创建文件描述符。
调用bind,将当前的文件描述符和IP/PORT绑定在一起,如果这个端口已经被其他进程占用了,就会bind失败。
调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备。
调用accept,并阻塞,等待客户端连接到来。
建立连接
而客户端在完成套接字创建后,就会在合适的时候通过connect函数向服务器发起连接请求,而客户端在connect的时候本质是通过某种方式向服务器三次握手,因此connect的作用实际就是触发三次握手。

建立连接的过程:

调用socket,创建文件描述符。
调用connect,向服务器发起连接请求。
connect会发出SYN段并阻塞等待服务器应答(第一次)。
服务器收到客户端的SYN,会应答一个SYN-ACK段表示“同意建立连接”(第二次)。
客户端收到SYN-ACK后会从connect返回,同时应答一个ACK段(第三次)。
这个建立连接的过程,通常称为三次握手。

需要注意的是,连接并不是立马建立成功的,由于TCP属于传输层协议,因此在建立连接时双方的操作系统会自主进行三次协商,最后连接才会建立成功。

数据传输的过程

在这里插入图片描述

数据交互

连接一旦建立成功并且被accept获取上来后,此时客户端和服务器就可以进行数据交互了。需要注意的是,连接建立和连接被拿到用户层是两码事,accept函数实际不参与三次握手这个过程,因为三次握手本身就是底层TCP所做的工作。accept要做的只是将底层已经建立好的连接拿到用户层,如果底层没有建立好的连接,那么accept函数就会阻塞住直到有建立好的连接。

而双方在进行数据交互时使用的实际就是read和write,其中write就叫做写数据,read就叫做读数据。write的任务就是把用户数据拷贝到操作系统,而拷贝过去的数据何时发以及发多少,就是由TCP决定的。而read的任务就是把数据从内核读到用户。

数据传输的过程“:

建立连接后,TCP协议提供全双工的通信服务,所谓全双工的意思是,在同一条连接中,同一时刻,通信双方可以同时写数据,相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据。
服务器从accept返回后立刻调用read,读socket就像读管道一样,如果没有数据到达就阻塞等待。
这时客户端调用write发送请求给服务器,服务器收到后从read返回,对客户端的请求进行处理,在此期间客户端调用read阻塞等待服务器端应答。
服务器调用write将处理的结果发回给客户端,再次调用read阻塞等待下一条请求。
客户端收到后从read返回,发送下一条请求,如此循环下去。

四次挥手的过程

在这里插入图片描述

端口连接

当双方通信结束之后,需要通过四次挥手的方案使双方断开连接,当客户端调用close关闭连接后,服务器最终也会关闭对应的连接。而其中一次close就对应两次挥手,因此一对close最终对应的就是四次挥手。

断开连接的过程:

如果客户端没有更多的请求了,就调用close关闭连接,客户端会向服务器发送FIN段(第一次)。
此时服务器收到FIN后,会回应一个ACK,同时read会返回0(第二次)。
read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN(第三次)。
客户端收到FIN,再返回一个ACK给服务器(第四次)。
这个断开连接的过程,通常称为四次挥手。
注意通讯流程与socket API之间的对应关系

在学习socket API时要注意应用程序和TCP协议是如何交互的:

应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect会发出SYN段。
应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read返回0就表明收到了FIN段。
为什么要断开连接?
建立连接本质上是为了保证通信双方都有专属的连接,这样我们就可以加入很多的传输策略,从而保证数据传输的可靠性。但如果双方通信结束后不断开对应的连接,那么系统的资源就会越来越少。

因为服务器是会收到大量连接的,操作系统必须要对这些连接进行管理,在管理连接时我们需要“先描述再组织”。因此当一个连接建立后,在服务端就会为该连接维护对应的数据结构,并且会将这些连接的数据结构组织起来,此时操作系统对连接的管理就变成了对链表的增删查改。

如果一个连接建立后不断开,那么操作系统就需要一直为其维护对应的数据结构,而维护这个数据结构是需要花费时间和空间的,因此当双方通信结束后就应该将这个连接断开,避免系统资源的浪费,这其实就是TCP比UDP更复杂的原因之一,因为TCP需要对连接进行管理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/368133.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

下载、安装Jenkins

进入官网 下载Jenkins https://www.jenkins.io 直接点击Download 一般是下长期支持版 因为它是java写的&#xff0c;你要运行它&#xff08;Jenkins.war&#xff09;肯定要有java环境 有两种方式去运行它&#xff0c;一种是下载Tomcat&#xff08;是很经典的java容器或者jav…

手写分布式存储系统v0.2版本

引言 上回说到 手写分布式存储系统v0.1版本 &#xff0c;已经实现了通过监听TCP端口并将数据写到本地磁盘的功能&#xff0c;今天咱们就继续往上面添砖加瓦 v0.2版本大致做以下功能 实现滚动写文件 代码优化 一、滚动写文件实现 由于咱们写文件是用的mmap进行文件写入&am…

2024美赛B题Searching for Submersibles原创论文完整版

Searching for Submersibles搜索潜水器 2024美赛B题Searching for Submersibles原创论文&#xff08;共38页&#xff09;部分内容&#xff0c;其余见文末&#xff1a; 整体框架&#xff1a; 1.1 问题背景与问题重述 海上游轮迷你潜水艇公司&#xff08;MCMS&#xff09;&…

算法基础——位运算,双指针,排序,二分

目录 1.位运算 与:& 或:| 取反&#xff1a;~ 异或&#xff1a;^或者是一个圈里有个加号的图像 移位:<<或者>> 例题:二进制中1的个数 例题&#xff1a;我们需要0 ​编辑 2.排序sort 例题&#xff1a;【模板】排序&#xff08;1&#xff09; 例题&…

大坑!react+thress.js

2. UI交互界面与Canvas画布叠加 | Three.js中文网 (webgl3d.cn) // canvas画布绝对定位 renderer.domElement.style.position absolute; renderer.domElement.style.top 0px; renderer.domElement.style.left 0px; renderer.domElement.style.zIndex -1; 我按照教程设置了…

红日三打靶!!!

红日三&#xff0c;黑盒测试 环境搭建一.外网打点1.网段探测2.端口服务扫描3.目录扫描4.网站漏洞扫描5.汇总&#xff0c;找破绽6.登陆MySQL改密码 7.进入后台&#xff0c;找能写马的地方8.蚁剑连接9.disable_functions绕过1.蚁剑插件绕过2.bypass_disablefunc_via_LD_PRELOAD绕…

AutoEncoder自动编码器、VAE变分自编码器、VQVAE量子化(离散化)的自编码器

文章目录 AutoEncoder自动编码器&#xff08;一&#xff09;AutoEncoder的基本架构&#xff08;二&#xff09;AutoEncoder的概率理解&#xff08;三&#xff09;AutoEncoder的局限 VAE变分自编码器&#xff08;Variational AutoEncoder&#xff09;&#xff08;一&#xff09;…

uni-app 经验分享,从入门到离职(三)——关于 uni-app 生命周期快速了解上手

文章目录 &#x1f4cb;前言⏬关于专栏 &#x1f3af;什么是生命周期&#x1f9e9;应用生命周期&#x1f4cc; 关于 App.vue/App.uvue &#x1f9e9;页面生命周期&#x1f4cc;关于 onShow 与 onLoad 的区别 &#x1f9e9;组件生命周期 &#x1f4dd;最后 &#x1f4cb;前言 这…

uniapp 组件封装

1. uniapp 组件封装时间戳格式化为星期 1.1. components/m-week.vue <template><text>{{week}}</text> </template> <script>export default {props: {time: String},mounted(e) {this.week this.getWeek(Number(this.time))},data() {return …

挑战杯 opencv 图像识别 指纹识别 - python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于机器视觉的指纹识别系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;4分 该项目较为新颖&#xff0c;适…

[python]基于opencv实现的车道线检测

【检测原理】 一、首先进行canny边缘检测&#xff0c;为获取车道线边缘做准备 二、进行ROI提取获取确切的车道线边缘&#xff08;红色线内部&#xff09; 三、利用概率霍夫变换获取直线&#xff0c;并将斜率正数和复数的线段给分割开来 四、离群值过滤&#xff0c;剔除斜率…

Java设计模式 – 四大类型

设计模式 – 四大类型 创建型模式结构型模式行为型模式J2EE模式 设计模式&#xff08;Design pattern&#xff09;是重构解决方案 根据书Design Patterns – Elements of Reusable Object-Oriented Software&#xff08;中文译名&#xff1a;设计模式 – 可复用的面向对象软件元…

lava学习-接口

接口-Interface 1.什么是接口&#xff1f; 例&#xff1a;构造器&#xff0c;代码块在接口中统统没有&#xff0c;也不能创建对象 构造器的使用-----实现类 例&#xff1a;下图中的B类就是一个 实现类 2.接口的好处 继承只能单继承&#xff0c;而接口可以弥补类单继承的不足&am…

【蓝桥杯】环形链表的约瑟夫问题

目录 题目描述&#xff1a; 输入描述&#xff1a; 输出描述&#xff1a; 示例1 解法一&#xff08;C&#xff09;&#xff1a; 解法二&#xff08;Cpp&#xff09;&#xff1a; 正文开始&#xff1a; 题目描述&#xff1a; 据说著名犹太历史学家 Josephus 有过以下故事&a…

作业2.3

一&#xff0e;选择题 1、适宜采用inline定义函数情况是&#xff08;C&#xff09; A. 函数体含有循环语句 B. 函数体含有递归语句‘、考科一 ’ C. 函数代码少、频繁调用 D. 函数代码多、不常调用 2、假定一个函数为A(int i4, int j0) {;}, 则执行“A (1);”语句后&…

有趣的CSS - css loading动画

Loading动画 整体效果核心代码html 代码&#xff1a;css 部分代码&#xff1a; 完整代码如下html 页面&#xff1a;css 样式&#xff1a;页面渲染效果&#xff1a; 整体效果 这个 Loading 效果主要用 css3 的 animation 属性配合 border 属性来实现的。 可以用作在下拉列表 Loa…

(bean配置类的注解开发)学习Spring的第十三天

bean配置类的注解开发 问题提出 用类充当配置文件 applicationcontext.xml : Configuration注解标识此类为配置类,替代原有xml文件 看原配置文件applicationcontext.xml代码 <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http:/…

微信小程序(三十二)本地异步储存API

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.同步和异步API的使用区别 2.异步API的缺陷 源码&#xff1a; index.wxml <!-- 列表渲染基础写法&#xff0c;不明白的看上一篇 --> <view class"students"><view class"item&q…

使用MATLAB驱动USRP-N320实现OFDM自收自发

文章目录 前言一、收发代码二、截取一帧 OFDM三、执行主函数四、运行结果五、资源自取 前言 本文作为实验结果记录及测试&#xff0c;方便后面回顾所做的工作。本文基于一台电脑和一台 USRP 设备实现了 OFDM 自发和自收功能 一、收发代码 ofdm_tx_rx_test.m 核心代码&#x…

C++迷宫游戏详解

个人主页&#xff1a;[PingdiGuo_guo] 收录专栏&#xff1a;[C干货专栏] 大家好呀&#xff0c;我是PingdiGuo_guo&#xff0c;今天我们来学习用C实现一个迷宫游戏。 目录 1.迷宫的具体步骤 1.1.迷宫的初始化 1.2.寻路算法 1.DFS算法 2.BFS算法 1.3.移动 2.总结 C迷宫游…