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

一、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_ENCRYPTIONstruct fscrypt_info	*i_crypt_info;
#endif#ifdef CONFIG_FS_VERITYstruct fsverity_info	*i_verity_info;
#endifvoid			*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_XMITstruct sk_buff*		(*sk_validate_xmit_skb)(struct sock *sk,struct net_device *dev,struct sk_buff *skb);
#endifvoid                    (*sk_destruct)(struct sock *sk);struct sock_reuseport __rcu	*sk_reuseport_cb;
#ifdef CONFIG_BPF_SYSCALLstruct bpf_local_storage __rcu	*sk_bpf_storage;
#endifstruct 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_NETFILTERerr = netfilter_init();if (err)goto out;
#endifptp_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);elseerr = __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.rhkb.cn/news/414416.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

超声波模块

HCSR04超声波模块是一种常用的测距模块&#xff0c;它通过检测超声波发射后遇到障碍物所反射的回波&#xff0c;从而测量出与障碍物之间的距离。以下是对HCSR04超声波模块的详细讲解&#xff1a; 一、模块组成与工作原理 组成&#xff1a;HCSR04超声波模块主要由两个压电陶瓷超…

Python一些可能用的到的函数系列131 发送钉钉机器人消息

说明 来自顾同学的助攻 钉钉机器人可以用来发送一些重要的系统消息&#xff0c;例如磁盘将满等等。原本还可以有更强的功能&#xff0c;就是监听群里的消息&#xff0c;然后做出反应&#xff0c;不过这个好像要买企业版&#xff0c;贵的毫无意义。 钉钉发消息有几种模式&#…

Ubuntu18.04 下安装CUDA

安装步骤 1.查看是否安装了cuda # 法1 cat /usr/local/cuda/version.txt # 法2 nvcc --version 2.若没有安装&#xff0c;则查看是否有N卡驱动&#xff0c;若无N卡驱动&#xff0c;则到软件与更新 -> 附加驱动中安装驱动 3.查看N卡驱动支持的cuda版本 nvidia-smi 如下…

VS+C++VTK-VCG三维网格模型鼠标框选拉普拉斯局部平滑

程序示例精选 VSCVTK-VCG三维网格模型鼠标框选拉普拉斯局部平滑 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《VSCVTK-VCG三维网格模型鼠标框选拉普拉斯局部平滑》编写代码&#xff0c;代…

扑捉一只耿鬼(HTML文件)

图例&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>耿鬼</title><style>body {background: #fff;font-family: Comfortaa, sans-serif;}* {box-sizing:…

【Kubernetes】持久卷的动态供给 Dynamic Provisioning

《持久化存储》系列&#xff0c;共包含以下文章&#xff1a; K8s 持久化存储方式持久卷 PV持久卷声明 PVC持久卷的动态供给 Dynamic Provisioning &#x1f60a; 如果您觉得这篇文章有用 ✔️ 的话&#xff0c;请给博主一个一键三连 &#x1f680;&#x1f680;&#x1f680; …

C++实现文件加解密及OllyDbg逆向

一.PE病毒和WannaCry勒索蠕虫 1.PE病毒 什么是PE病毒&#xff1f; PE病毒是以Windows PE程序为载体&#xff0c;能寄生于PE文件或Windows系统的病毒程序。PE病毒数量非常之多&#xff0c;包括早起的CIH病毒&#xff0c;全球第一个可以破坏计算机硬件的病毒&#xff0c;它会破…

Linux和Unix的区别及为什么鸿蒙系统不用Unix的原因

目录 Linux是什么? Unix是什么&#xff1f; 他们的区别&#xff1a; 鸿蒙系统介绍及鸿蒙系统不用Unix的原因 Linux是什么? Linux的历史可以追溯到1991年&#xff0c;由芬兰的计算机科学家林纳斯托瓦兹&#xff08;Linus Torvalds&#xff09;为了学习操作系统的工作原理而…

计算机毕设选题推荐-基于python的豆瓣电子图书数据可视化分析

&#x1f496;&#x1f525;作者主页&#xff1a;毕设木哥 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; 实战项目 文章目录 实战项目 一、基于python的豆瓣电子图书数…

计算机毕业设计Hadoop+Spark抖音可视化 抖音舆情监测 预测算法 抖音爬虫 抖音大数据 情感分析 NLP 自然语言处理 Hive 机器学习 深度学习

技术栈&#xff1a;数据分析Spark、数据库Hive MySQL、服务器djano、爬虫requests jieba库中文分词&#xff0c;通俗来说&#xff0c;就是将一句(段)话按一定的规则(算法)拆分成词语、成语、单个文字。 中文分词是很多应用技术的前置技术&#xff0c;如搜索引擎、机器翻译、词…

SprinBoot+Vue学生选课小程序的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue3.6 uniapp代码 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平…

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

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

tecplot宏批量导入数据

Tecplot新手进阶——使用tecplot宏操作批量处理数据输出图片&#xff08;详细步骤&#xff09; tecplot 宏的使用方法及代码改写 第一步&#xff1a;首先点击Scripting–>Record Macro&#xff0c;生成一个脚本文件&#xff0c;即.mcr文件 点击保存&#xff0c;会出现这个…

SpringBoot实战:Spring Boot项目使用SM4国密加密算法

引言 在业务系统构建与部署的环节中&#xff0c;数据库作为核心存储组件&#xff0c;其连接信息的安全至关重要。通常情况下&#xff0c;这些敏感信息&#xff0c;如数据库密码&#xff0c;会直接以明文形式存储在YAML配置文件中&#xff0c;这无疑增加了信息泄露的风险。为有效…

活动系统开发之采用设计模式与非设计模式的区别-需求整理

用户需求(活动系统)&#xff1a; 1、活动类型&#xff1a;答题、图片展示、签到、抽奖、组团等活动 2、活动介绍&#xff1a; a、答题活动&#xff1a; 第一种是签到后&#xff0c;随机抽取10道题&#xff0c;答对8到就可以抽奖&#xff1b; 第二种是随机抽取一道题&#xff0…

Call openai-node in the backend or call https in the frontend?

题意&#xff1a;在后端调用 openai-node 还是在前端调用 https&#xff1f; 问题背景&#xff1a; I have a web application by ReactJS and Nodejs. This application calls OpenAI APIs. 我有一个使用 ReactJS 和 Node.js 开发的 Web 应用程序。这个应用程序调用 OpenAI …

UE5开发——射击武器类拾取

整体框架&#xff1a; 拾取武器 要在 Unreal Engine 5 (UE5) 中实现一个按 E 键拾取武器的功能&#xff0c;您可以遵循以下步骤&#xff1a; ### 步骤 1: 创建拾取物品的基础类 1. 在 Content Browser 中创建一个新的 C 类&#xff0c;继承自 AActor 或者 AStaticMeshActor。…

串口助手使用和插入usb转TTL的COM口识别问题

问题出现原因 由于串口调试中经常需要通过断电对单片机烧录程序&#xff0c;所以制作了一个转接带开关的USB 转接口&#xff0c;如下图所示&#xff0c;其中按键控制的是OUT口的电源通断。但为了能够数据传输&#xff0c;有两根传输数据的线是一直连接的。在使用usb进行程序烧…

什么是EDR、NDR、MDR、XDR?他们之间什么区别?

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 概述 EDR是什…

ParallelsDesktop19可在任何Mac上运行Windows软件

ParallelsDesktop19是一款Mac虚拟机软件&#xff0c;可在任何Mac上运行Windows&#xff0c;体验不同操作系统之间的无缝集成&#xff0c;并具有创新设计和增强功能&#xff0c;如无密码登录与TouchID、支持macOSSonoma14和增强打印选项。此外&#xff0c;它还支持运行更多Windo…