c++应用网络编程之八SOCKET探究

news2024/9/22 4:03:45

一、socket

在目前主流的网络通信中,SOCKET编程其实就是网络编程的代名词。在前面反复提到socket,那么socket到底是什么呢?英文的愿意是“插座、槽”的意思。这里虽然不讲解传统的网络协议但不得不简单说明一下。
首先从宏观上看,一般的网络编程(也就套接字编程),主要分成几大块,即上层的应用程序;其下为OS相关的网络协议栈,它包括应用层(如TELNET,FTP,SMTP等),传输层(TCP,UDP等),网络层(IP)以及数据链路层;网卡驱动以及相关硬件(含固件程序)。
而在协议栈中,教科书中的OSI协议栈其实没有现实意义。实际的网络部署基本全是TCP/IP的协议栈。而在这个协议栈,网络编程实际应用的基本以TCP/IP,UDP为主。即第二层和第三层。应用层一般是提供的命令操作而更下层链路层基本是对网络进行管理时才会用到。上层应用开发很难直接面对这层进行处理。所以在内核中一般会针对上层应用和下层接口提供两套类似的相关数据结构相关定义。协议栈中,网络数据的流动是双向进行的,可以理解成净水器的过滤,只不过这种“过滤”要抽象的来理解,向上传输时是解包,而向下传输时是封包。
那么问题来了,Socket与TCP,UDP,IP是什么关系?为什么Socket编程即套接字编程可以代表网络编程?
一个个来回答,首先要实现一个通信,最基础的是要有通信的协议,也就是如何通信来保障双方可以理解和支持。就和人们讲话一样,讲一个知识点前先要把其中的一些名词术语解释清楚,这样才能更好的沟通。再如兑换不同的国家的货币,也需要一个锚定的货币来进行(一般是美元)。有了这个协议,就需要有一个东西做其与OS以及上层应用程序的接口定义或者说一个代言人,而这个代言人就是Socket。
理解到这一点上,重点就来了。首先要继续理解,socket本身是一个文件句柄或者说一个节点或者说一个ID,不管怎么理解,先把这一点记清楚。然后,它并是一个简单的fd,它代表一个套接字的接口,所以从宏观角度看,它可以是一个三元组,四元组或者五元组。以五元组为例,它是本地的IP地址和端口,远端的IO和端口以及协议类型。这个几元组才是重点中的关键,因为它就是socket接口代表的真正的东西,即实际在OS及协议栈中的通信的相关接口和数据结构的显示的声明。虽然开发者无法看到具体的内容,但实际上就是通过这些,实现了对底层的网络通信的指定和控制。

二、socket通信的基本流程

现在以服务端为例子,解释一下Socket通信的原理。其基本的流程图如下:

在这里插入图片描述

在套接字的编程过程中,相对客户端网络编程,服务端编程是非常复杂的。但它可以基本抽象出上面的流程:
1、创建Socket fd,提供套接字编程的文件句柄
2、通过bind()函数绑定到相关资源上,重点是端口值
3、开始在这个Socket上进行监听,并处理监听到的客户端连接Socket(这就是listen后跟的长度的值,当然不同的OS可能有所不同)
4、通过accept()函数等待客户端的连接请求,并在有连接请求时不断处理其生成一个新连接Socket fd
5、通过上面新生成的socket fd,开始进行接收和发送数据的操作(根据情况可以循环处理)
6、结束后,结束相关网络操作并回收socket资源
而对客户端来说,如果不考虑各种异常情况就非常简单了:
1、创建socket fd
2、利用connect()函数连接服务端地址
3、利用连接成功的socket fd发送接收数据
4、结束相关操作并清理回收资源
UDP通信与之类似,不过少了一些相关的步骤而已,请大家自行查阅相关资料。

三、原理

在分析过socket是个什么东西后,又明白了其通信的流程,那么在上面提到的socket是接口,是数据结构,是对上对下的一个代理是怎么得来的呢?下面看一下在内核中对其的处理,就明白了。
先看一下内核中socket的文件系统的定义:

struct inode {
	umode_t			i_mode;
	unsigned short		i_opflags;
	kuid_t			i_uid;
	kgid_t			i_gid;
	unsigned int		i_flags;

......

#ifdef CONFIG_FSNOTIFY
	__u32			i_fsnotify_mask; /* all events this inode cares about */
	struct fsnotify_mark_connector __rcu	*i_fsnotify_marks;
#endif

#ifdef CONFIG_FS_ENCRYPTION
	struct fscrypt_info	*i_crypt_info;
#endif

#ifdef CONFIG_FS_VERITY
	struct fsverity_info	*i_verity_info;
#endif

	void			*i_private; /* fs or device private pointer */
} __randomize_layout;
struct socket {
	socket_state		state;

	short			type;

	unsigned long		flags;

	struct file		*file;
	struct sock		*sk;
	const struct proto_ops	*ops;

	struct socket_wq	wq;
};
struct sock {
	/*
	 * Now struct inet_timewait_sock also uses sock_common, so please just
	 * don't add nothing before this first member (__sk_common) --acme
	 */
	struct sock_common	__sk_common;
#define sk_node			__sk_common.skc_node
#define sk_nulls_node		__sk_common.skc_nulls_node
#define sk_refcnt		__sk_common.skc_refcnt
#define sk_tx_queue_mapping	__sk_common.skc_tx_queue_mapping
#ifdef CONFIG_SOCK_RX_QUEUE_MAPPING
#define sk_rx_queue_mapping	__sk_common.skc_rx_queue_mapping
#endif

#define sk_dontcopy_begin	__sk_common.skc_dontcopy_begin
#define sk_dontcopy_end		__sk_common.skc_dontcopy_end
#define sk_hash			__sk_common.skc_hash
......
	void			(*sk_state_change)(struct sock *sk);
	void			(*sk_data_ready)(struct sock *sk);
	void			(*sk_write_space)(struct sock *sk);
	void			(*sk_error_report)(struct sock *sk);
	int			(*sk_backlog_rcv)(struct sock *sk,
						  struct sk_buff *skb);
#ifdef CONFIG_SOCK_VALIDATE_XMIT
	struct sk_buff*		(*sk_validate_xmit_skb)(struct sock *sk,
							struct net_device *dev,
							struct sk_buff *skb);
#endif
	void                    (*sk_destruct)(struct sock *sk);
	struct sock_reuseport __rcu	*sk_reuseport_cb;
#ifdef CONFIG_BPF_SYSCALL
	struct bpf_local_storage __rcu	*sk_bpf_storage;
#endif
	struct rcu_head		sk_rcu;
	netns_tracker		ns_tracker;
}
static const struct super_operations sockfs_ops = {
	.alloc_inode	= sock_alloc_inode,
	.free_inode	= sock_free_inode,
	.statfs		= simple_statfs,
};
struct socket_alloc {
	struct socket socket;
	struct inode vfs_inode;
};

注意上面的socket结构体和sock结构体,就是前文提到的针对不同的层实现的两类socket的描述,这和现实世界中对一个人在不同的场景的称呼有所不同的意义有些类似,一定要注意认识清楚。
socket fd其实就是内核中的sockfs,是vfs(Virtual File System,虚拟文件系统)中的一类,它位于vfs之下。不过需要说明的是,在vfs中,基础的数据定义就是indoe数据结构。在不同的文件类中,indoe会根据实际情况进行相关类型的转换或者数据处理。比如常见的ext4,就需要进行地址的转换。而socket亦是如此,它可以理解成是在inode上层进行的一个封装或者一个抽象或者一个转换,看怎么理解更容易就怎么理解。
这也是为什么socket同样可以使用接口read,write进行操作的原因。写得比较多的开发者可能就用过这两个函数。
Linux中文件系统使用的是索引节点(innode)的数据结构来管理文件的相关元数据。而其元数据本身的组织也是常见的B+树树。使用树的优点很多,特别是对于文件这种本身就是一个级联的形势的场景下更有优势。
再看一下其创建Socket相关代码:

static int __init sock_init(void)
{
	int err;
	/*
	 *      Initialize the network sysctl infrastructure.
	 */
	err = net_sysctl_init();
	if (err)
		goto out;

	/*
	 *      Initialize skbuff SLAB cache
	 */
	skb_init();

	/*
	 *      Initialize the protocols module.
	 */

	init_inodecache();

	err = register_filesystem(&sock_fs_type);
	if (err)
		goto out;
	sock_mnt = kern_mount(&sock_fs_type);
	if (IS_ERR(sock_mnt)) {
		err = PTR_ERR(sock_mnt);
		goto out_mount;
	}

	/* The real protocol initialization is performed in later initcalls.
	 */

#ifdef CONFIG_NETFILTER
	err = netfilter_init();
	if (err)
		goto out;
#endif

	ptp_classifier_init();

out:
	return err;

out_mount:
	unregister_filesystem(&sock_fs_type);
	goto out;
}
static struct inode *sock_alloc_inode(struct super_block *sb)
{
	struct socket_alloc *ei;

	ei = alloc_inode_sb(sb, sock_inode_cachep, GFP_KERNEL);
	if (!ei)
		return NULL;
	init_waitqueue_head(&ei->socket.wq.wait);
	ei->socket.wq.fasync_list = NULL;
	ei->socket.wq.flags = 0;

	ei->socket.state = SS_UNCONNECTED;
	ei->socket.flags = 0;
	ei->socket.ops = NULL;
	ei->socket.sk = NULL;
	ei->socket.file = NULL;

	return &ei->vfs_inode;
}

再看一些常见的上层应用用到的接口函数的实现:

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
	unsigned long a[AUDITSC_ARGS];
	unsigned long a0, a1;
	int err;
	unsigned int len;

	if (call < 1 || call > SYS_SENDMMSG)
		return -EINVAL;
	call = array_index_nospec(call, SYS_SENDMMSG + 1);

	len = nargs[call];
	if (len > sizeof(a))
		return -EINVAL;

	/* copy_from_user should be SMP safe. */
	if (copy_from_user(a, args, len))
		return -EFAULT;

	err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
	if (err)
		return err;

	a0 = a[0];
	a1 = a[1];

	switch (call) {
	case SYS_SOCKET:
		err = __sys_socket(a0, a1, a[2]);
		break;
	case SYS_BIND:
		err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
		break;
	case SYS_CONNECT:
		err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
		break;
	case SYS_LISTEN:
		err = __sys_listen(a0, a1);
		break;
	case SYS_ACCEPT:
		err = __sys_accept4(a0, (struct sockaddr __user *)a1,
				    (int __user *)a[2], 0);
		break;
	case SYS_GETSOCKNAME:
		err =
		    __sys_getsockname(a0, (struct sockaddr __user *)a1,
				      (int __user *)a[2]);
		break;
	case SYS_GETPEERNAME:
		err =
		    __sys_getpeername(a0, (struct sockaddr __user *)a1,
				      (int __user *)a[2]);
		break;
	case SYS_SOCKETPAIR:
		err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
		break;
	case SYS_SEND:
		err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
				   NULL, 0);
		break;
	case SYS_SENDTO:
		err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
				   (struct sockaddr __user *)a[4], a[5]);
		break;
	case SYS_RECV:
		err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
				     NULL, NULL);
		break;
	case SYS_RECVFROM:
		err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
				     (struct sockaddr __user *)a[4],
				     (int __user *)a[5]);
		break;
	case SYS_SHUTDOWN:
		err = __sys_shutdown(a0, a1);
		break;
	case SYS_SETSOCKOPT:
		err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
				       a[4]);
		break;
	case SYS_GETSOCKOPT:
		err =
		    __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
				     (int __user *)a[4]);
		break;
	case SYS_SENDMSG:
		err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
				    a[2], true);
		break;
	case SYS_SENDMMSG:
		err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
				     a[3], true);
		break;
	case SYS_RECVMSG:
		err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
				    a[2], true);
		break;
	case SYS_RECVMMSG:
		if (IS_ENABLED(CONFIG_64BIT))
			err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
					     a[2], a[3],
					     (struct __kernel_timespec __user *)a[4],
					     NULL);
		else
			err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
					     a[2], a[3], NULL,
					     (struct old_timespec32 __user *)a[4]);
		break;
	case SYS_ACCEPT4:
		err = __sys_accept4(a0, (struct sockaddr __user *)a1,
				    (int __user *)a[2], a[3]);
		break;
	default:
		err = -EINVAL;
		break;
	}
	return err;
}
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
	int ret = -EBADF;
	struct fd f;

	f = fdget(fd);
	if (f.file) {
		struct sockaddr_storage address;

		ret = move_addr_to_kernel(uservaddr, addrlen, &address);
		if (!ret)
			ret = __sys_connect_file(f.file, &address, addrlen, 0);
		fdput(f);
	}

	return ret;
}

这些代码都来自于Linux5.19中,有兴趣可以自己跟进去看一看,可能不同的平台实现略有不同,需要大家注意。
进行过网络通信开发的实践的开发者知道,Socket从创建到监听到接收等有着一系列的返回情况,它们之间有着细节的不同。一般来说,Socket可以大概分成两类,即监听套按字和普通套按字。所谓监听套接字是指listen和Accept函数,它们只负责连接的管理而不进行数据的处理,更直白一些说,它只得到socket而不会做其它的事情。而普通套接字则比较全面,它也是开发者常说的网络IO,即通过对它的操作可以传输和得到数据。
这里还需要澄清一个问题,socket所谓的读写,一般并不是指直接从网卡读写数据,而是对缓冲区数据的读写。同样,缓冲区也分成多层,有程序的,有驱动的,有网卡的等等。这个在DPDK系列中也进行过分析。有兴起的可以看看相关的资料。
另外,为了完成指定的目标,这些套接字都注册了相关的句柄事件。说到这儿,可能大家就非常清楚了,在编写Select,Epoll等模型时,事件的处理的来历了。也明白了,为什么叫事件驱动模型。比如常见的如监听类型的Socket会触发接收和连接等。而Send和Recv等会触发读写事件等等。当然还有其它如悬挂或异常等都需要有专门的处理事件,这都需要开发者在实际开发中进行全面的管理,否则极容易出现异常导致整个程序的Crash。
套接字socket的分类,监听,普通,接收

四、总结

我们分析完成了一般网络通信的原理和相关的IO通信的机制,已经基本明白了网络通信到底是怎么一回事,以及其相关的难点和重点。国内的教科书往往有一个重要的问题,就是从来不连贯的讲述一个知识点,而是将这些知识点打乱并且乱序讲解。这样做很难让一个人把网络通信的相关知识融会贯通。换句话说,基本上靠自觉把相关的体系建立起来的可能性极小。这也是本系列一开篇并没有像传统的书本把网络基础知识大讲一通,然后再搞几个IO模型举几个例子,基本上这书就过去了。
当然,网上也有一些优秀的书籍和资料,但又非常倾向于使用实际例子来解释说明,让一些初学者感到压力很大。希望现在这个系列能给一些初级和中级的开发者一些从理论高度把握网络通信整体的情况再深入到例程中学习实践的思路。
一花开放不是春,百花齐放春来到。与诸君共勉!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《数字信号处理》学习01-离散时间信号与序列的卷积和运算

目录 一&#xff0c;信号 二&#xff0c;序列的运算 1&#xff0c;卷积和 2&#xff0c;matlab实现 相关的电子书籍请到这篇文章所在的专栏&#xff0c;并通过夸克网盘链接下载。 很多简单的知识点我就不再赘述了&#xff0c;接下来就着重记录我学习过程中遇到的较难理…

class 3: vue.js 3 计算属性

计算属性是一种Options API&#xff0c;Options API是一种通过对象定义属性、方法等框架API的方式我们知道&#xff0c;在模板中可以直接通过插值语法显示一些data属性中的数据。但是在某些情况下&#xff0c;可能需要对数据进行一些转化操作之后再显示&#xff0c;或者需要将多…

PostgreSQL技术内幕5:PostgreSQL存储引擎从磁盘到内存的读取

文章目录 0.简介1.背景知识1.1 计算机存储结构1.2 数据库常见的磁盘和内存访问形式 2. 整体获取层次3.元组介绍4. Buffer管理4.1 Buffer组成4.2 修改后落盘4.3 获取buffer页的流程 5.存储管理器&#xff08;SMGR)6.磁盘管理器&#xff08;MD)7.虚拟文件管理器&#xff08;VFD)8…

【攻防世界新手入门】simple_js

小宁发现了一个网页&#xff0c;但却一直输不对密码。&#xff08;Flag格式为 Cyberpeace{xxxxxxxxx} &#xff09; 该题来自攻防世界新手练习区的 GFSJ0480。 访问靶场&#xff0c;输入任意密码&#xff0c;弹出错误窗口并加载空页面&#xff0c;使用检查器查看对应源代码&…

zdppy+vue3+onlyoffice文档管理系统实战 20240901 上课笔记 基于验证码登录功能基本完成

遗留的问题 1、点击切换验证码2、1分钟后自动切换验证码 点击切换验证码 实现步骤&#xff1a; 1、点击事件2、调用验证码接口3、更新验证码的值 点击事件 给图片添加点击事件&#xff1a; <img :src" data:image/png;base64, captchaImg"style"widt…

ffplay源码分析(五)包缓存队列和帧缓存队列

在音视频处理流程中&#xff0c;ffplay的有两种队列&#xff0c;包缓存队列&#xff08;Packet Buffer Queue&#xff09;和帧缓存队列&#xff08;Frame Buffer Queue&#xff09;。这两个队列的存在&#xff0c;是为了适应音视频数据处理过程中的多线程架构——包括收包线程、…

win11+vscode+Flutter 开发环境配置

https://blog.csdn.net/Oven_maizi/article/details/126804404 1 vscode插件 安装 安装红框中的两个 2 flutter sdk 安装 dart sdk 包含在flutter sdk 里面&#xff0c;路径&#xff1a;flutter_windows_3.24.1-stable\flutter\bin\cache\dart-sdk 方式1&#xff1a; 通过…

[001-07-001].Redis7缓存双写一致性之更新策略探讨

1、面试题&#xff1a; 1.只要使用缓存&#xff0c;就可能会涉及到redis缓存与数据库双存储双写&#xff0c;只要是双写&#xff0c;就存在数据一致性问题&#xff0c;那么是如何解决数据一致性问题的2.双写一致性&#xff0c;你先动缓存redis还是数据库MySQL&#xff0c;哪一个…

Python爬虫案例四:爬取某个博主的所有文章保存成PDF格式

引入&#xff08;将图片保存成PDF格式&#xff09;&#xff1a; 测试链接&#xff1a; https://zq.bookan.com.cn/?tdetail&id21088&ct1&is31042341&rid4658&#xff08;图书馆图片保存PDF&#xff09;&#xff0c;前提是装库&#xff0c;pip install img2pdf…

IDEA如何将某个文件夹内的多个module合并到一个大的module内

效果&#xff1a; 初始 方法 Ctrl shift Alt S 打开 project structure 导入module 将这个大文件夹作为新的module导入 效果图 完事儿

C#上位机采用数据库操作方式对Excel或WPS表格进行读取操作

C#采用数据库操作方式对Excel或WPS表格进行读取操作 1、创建连接字符串并编写一个进行数据库操作的方法 public class OleDbHelper{//创建连接字符串private static string connString "ProviderMicrosoft.ACE.OLEDB.12.0;Data Source{0};" "Extended Propert…

如何在态势中嵌入感知?

态势感知是对事物发展的各种情况的估计、判断和理解&#xff0c;而感知是通过感官对事物的认识和了解。要在态势过程中嵌入感知过程&#xff0c;你可以考虑以下步骤&#xff1a; 1、确定态势和感知的要素&#xff1a;首先&#xff0c;明确态势和感知所涉及的关键要素。态势可能…

Lesson08---string类(1)

1. 为什么要学习string类 2. 标准库中的string类 3. string类的模拟实现 4. 扩展阅读 1. 为什么学习string类&#xff1f; 1.1 C语言中的字符串 在c语言中没有string类型只有char类型&#xff0c;char类型只能是一个字符非常的不好用&#xff0c;想要字符串还必须要弄一个c…

华溢艺术之花在社区绽放

华溢艺术之花在社区绽放 这是一个阳光明媚的周末&#xff0c;华溢艺术少儿艺术体验走进了夷陵区营盘社区碧桂园小区。这次活动不仅为社区的孩子们带来了丰富多彩的艺术体验&#xff0c;还让家长们目睹了孩子们在艺术成长道路上那些优美动人的闪光点。 活动现场&#xff0c;热闹…

线性代数|机器学习-P32循环矩阵的特征向量-傅里叶矩阵

文章目录 1. 大纲2. 循环矩阵2.1 移位矩阵P2.2 P的特征值和特征向量2.3 循环卷积矩阵2.4 循环卷积计算 3. 傅里叶矩阵 1. 大纲 循环矩阵在机器学习&#xff0c;图像处理中的应用循环卷积矩阵的特征值&#xff0c;特征向量&#xff0c;卷积规则循环卷积矩阵多项式表达&#xff…

kaggle平台free使用GPU

1、注册 请保证在【科学上网】条件下进入如下操作&#xff0c;只有在注册账户和手机号验证时需要。 step1&#xff1a;注册账户 进入kaggle官网&#xff1a;https://www.kaggle.com/&#xff0c;点击右上角【Register】进入注册页面 最好选择使用邮箱注册&#xff08;&#…

零域(微隔离)详述

一、什么是零信任微隔离&#xff1f; 微隔离的实现方式是将数据中心内部所有的业务按照特定的原则划分为数个微小的网络节点&#xff0c;根据动态策略分析对这些节点执行访问控制&#xff0c;在逻辑上将这些节点隔离开&#xff0c;限制用户横向移动&#xff0c;这就是微…

docker-compose 快速部署nacos-standalone单节点

一、nacos 介绍 官网&#xff1a; https://nacos.io/ 一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台 二、如何使用docker-compose 快速部署nacos2.3.1 ⚠️ &#xff1a; nacos-standalone 部署方式 依赖于 数据库&#xff0c;请先配置好数据库实例&…

日系编曲:了解日系音乐 日系和声特征 设计日系和声 和弦进行摘抄

了解日系音乐 日系音乐风格多样&#xff0c;涵盖流行、摇滚、民谣、古典等多种类型。以下是部分知名的日系音乐作品、歌手及乐队&#xff1a; 作品 《First Love》是宇多田光的代表作之一&#xff0c;旋律悠扬&#xff0c;情感真挚&#xff0c;展现了初恋的美好与青涩&#xf…

c++ 原型模式

文章目录 什么是原型模式为什么要使用原型模式使用场景示例 什么是原型模式 用原型实例指定创建对象的种类&#xff0c;并通过拷贝这些原型创建新的对象&#xff0c;简单理解就是“克隆指定对象” 为什么要使用原型模式 原型模式&#xff08;Prototype Pattern&#xff09;是…