前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >套接字Socket编程

套接字Socket编程

作者头像
JavaEdge
发布于 2021-12-07 04:22:42
发布于 2021-12-07 04:22:42
1.3K00
代码可运行
举报
文章被收录于专栏:JavaEdgeJavaEdge
运行总次数:0
代码可运行

Socket,原意插座、插口。写软件程序时,可以想象成一根网线,一头插在客户端,一头插在服务端,然后进行通信。所以通信前,双方都要建立一个Socket。

Socket编程进行的是端到端的通信,意识不到中间经过多少局域网、路由器,因而能设置参数,也只能是端到端协议之上网络层和传输层的。

在网络层,Socket函数需要指定IPv4 or IPv6,分别对应设置为:

  • AF_INET
  • AF_INET6

还要指定到底是TCP还是UDP

  • TCP协议是基于数据流的,所以设置为SOCK_STREAM
  • UDP是基于数据报的,因而设置为SOCK_DGRAM

基于TCP协议的Socket程序函数调用过程

两端创建了Socket之后,接下来的过程中,TCP和UDP稍有不同,我们先来看TCP。

TCP的服务端要先监听一个端口,一般是先调用bind函数,给这个Socket赋予一个IP地址和端口。

为什么需要端口? 一个应用程序,当一个网络包来了,内核要通过TCP头里面的这个端口,找到你这个应用程序,把包给你。

为什么要IP地址? 有时一台机器会有多个网卡,就会有多个IP地址。可以选择监听所有网卡,也可以选择监听一个网卡,这样,只有发给这个网卡的包,才会给你。

当服务端有了IP和端口号,就能调用listen函数进行监听。服务端就进入listen状态,这时客户端即可发起连接。

在内核中,为每个Socket维护两个队列:

  • 已经建立了连接的队列,这时候连接三次握手已经完毕,处于established状态
  • 还没有完全建立连接的队列,这时三次握手还没完成,处于syn_rcvd状态。

接着服务端调用accept函数,拿出一个已完成的连接进行处理。若还没完成,就要等着。

在服务端等待时,客户端可通过connect函数发起连接:

  • 先在参数中指明要连接的IP地址和端口号
  • 然后开始发起三次握手 内核会给客户端分配一个临时端口。一旦握手成功,服务端的accept就会返回另一个Socket

监听的Socket和真正用来传数据的Socket是两个:

  • 监听Socket
  • 已连接Socket

连接建立成功之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。

基于TCP协议的Socket程序函数调用过程。

TCP的Socket就是一个文件流,因为Socket在Linux中是以文件形式存在。 写入和读出都是通过文件描述符(后文简称为 fd)。

在内核中,Socket是一个文件,那对应就有fd。 每个进程都有一个数据结构task_struct,里面指向一个fd数组,列出该进程打开的所有文件的fd。 fork 之后,就会创建个该结构:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct task_struct {
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	long signal;
	fn_ptr sig_restorer;
	fn_ptr sig_fn[32];
/* various fields */
	int exit_code;
	unsigned long end_code,end_data,brk,start_stack;
	long pid,father,pgrp,session,leader;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	long alarm;
	long utime,stime,cutime,cstime,start_time;
	unsigned short used_math;
/* file system info */
	int tty;		/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];
/* tss for this task */
	struct tss_struct tss;
};

fd是个整数,是这个数组的下标。

数组内容是个指针,指向内核中所有打开的文件的列表。是文件,就会有个inode,Socket对应的inode不像真正的文件系统保存在硬盘,而是在内存。在这个inode,指向了Socket在内核中的Socket结构。

这个结构里面,主要是两个队列:

  • 发送队列
  • 接收队列

这两个队列里保存的是一个缓存sk_buff。该缓存能够看到完整的包结构。

基于UDP协议的Socket程序函数调用过程

UDP没有连接,所以无需三次握手,即无需调用listen、connect。 但UDP的的交互仍需IP、端口号,因而也需要bind。

UDP没有维护连接状态,因而无需每对连接都建立一组Socket,只要有一个Socket就能和多个客户端通信。 正因为没有连接状态,每次通信时,都调用sendto、recvfrom,都可以传入IP地址和端口。 基于UDP协议的Socket程序函数调用过程

服务器如何接更多的项目?

建立连接后,进行一个while循环:

  • 客户端发了收
  • 服务端收了发

这只是网络编程第一步,使用这种方法,只能一对一沟通。 若你是个服务器,同时只能服务一个客户,那肯定不行。那我肯定能接的服务越多越好。

那理论值是多少呢?即最大连接数,系统会用一个四元组来标识一个TCP连接。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{本机IP, 本机端口, 对端IP, 对端端口}

服务器通常固定在某个本地端口上监听,等待客户端的连接请求。 因此,服务端的TCP连接四元组只有对端IP,即客户端的IP和对端端口,也即客户端的端口是可变的,因此:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
最大TCP连接数=客户端IP数 × 客户端端口数

比如最常用的IPv4:

  • 客户端的IP数,max=2^32
  • 客户端的端口数,max=2^16

即服务端单机最大TCP连接数,约为2^48。

服务端最大并发TCP连接数远不能达到理论上限:

  • fd限制 Socket都是文件,所以要通过ulimit配置fd的数目
  • 内存 按上面的数据结构,每个TCP连接都要占用一定内存,os有限。 所以,在资源有限情况下,要想服务更多客户,就得降低每个客户消耗的资源数。 那有哪些方案呢?

多进程

将项目外包给其他公司,相当于你是个代理,在那里监听请求。一旦建立了一个连接,就会有一个已连接Socket,这时你可以创建一个子进程,然后将基于已连接Socket的交互交给这个新的子进程来做。 就像来了个新项目,但项目不一定你做,可以再注册一家子公司,招点人,然后把项目转包给这家子公司做,以后对接就交给这家子公司,你就可以专注接新的项目了。

问题是你如何创建子公司,又怎么将项目交给子公司?

Linux使用fork创建子进程,基于父进程完全拷贝一个子进程。在Linux内核中,会复制fd的列表,也会复制内存空间,还会复制一条记录当前执行到了哪行程序的进程。 显然,复制的时候在调用fork,复制完后,父进程、子进程都会记录当前刚刚执行完的fork。 这两个进程刚复制完时,基本一样,只是根据fork返回值区分:

  • 返回值是0,则是子进程
  • 返回值是其它整数,就是父进程

进程复制过程

因为复制了fd列表,而fd都是指向整个内核统一的打开文件列表的,因而父进程刚才因为accept创建的已连接Socket也是一个文件描述符,同样也会被子进程获得。

接下来,子进程就可以通过这个已连接Socket和客户端进行互通了,当通信完毕之后,就可以退出进程,那父进程如何知道子进程干完了项目,要退出呢?还记得fork返回的时候,如果是整数就是父进程吗?这个整数就是子进程的ID,父进程可以通过这个ID查看子进程是否完成项目,是否需要退出。

多线程

将项目转包给独立的项目组,之前的方案若每次接个项目,都申请一个新公司,然后干完了,就注销掉这个公司,实在太消耗精力。毕竟一个新公司要有新公司的资产,有新的办公家具,每次都买了再卖,不划算。

考虑使用线程,相比于进程,更轻量级。

  • 创建进程相当于成立新公司,购买新办公家具
  • 创建线程,就相当于在同一个公司成立项目组。一个项目做完了,那这个项目组就可以解散,组成另外的项目组,办公家具还可复用。

Linux通过pthread_create创建一个线程,也调用do_fork。 虽然新线程在task列表会新创建一项,但很多资源,例如fd列表、进程空间,还是共享的,只不过多了一个引用。

新的线程也可以通过已连接Socket处理请求,达到并发处理的目的。

基于进程或线程模型其实还有问题。新到来一个TCP连接,就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。 C10K,一台机器要维护1万个连接,就要创建1万个进程或线程吗,那操作系统无法承受。如果维持1亿用户在线需要10万台服务器,成本也太高了。

C10K问题就是你接项目接的太多了,如果每个项目都成立单独的项目组,就要招聘10万人,你肯定养不起,那怎么办呢?

IO多路复用,一个线程维护多个Socket

一个项目组支撑多个项目,这时每个项目组都应该有个项目进度墙,将自己组看的项目列在那里,然后每天通过项目墙看每个项目的进度,一旦某个项目有了进展,就派人去盯一下。

由于Socket是文件描述符,因而某个线程盯的所有的Socket,都放在一个文件描述符集合fd_set中,这就是项目进度墙,然后调用select函数来监听文件描述符集合是否有变化。 一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在fd_set对应的位都设为1,表示Socket可读或者可写,从而可以进行读写操作,然后再调用select,接着盯着下一轮的变化。

IO多路复用,从“派人盯着”到“有事通知”

一个项目组支撑多个项目,上面select函数还是有问题,因为每次Socket所在的文件描述符集合中有Socket发生变化的时候,都需要通过轮询,需要将全部项目都过一遍,这大大影响了一个项目组能够支撑的最大的项目数量。因而使用select,能够同时盯的项目数量由FD_SETSIZE限制。

改成事件通知的方式,就会好很多,项目组不需要通过轮询挨个盯着这些项目,而是当项目进度发生变化的时候,主动通知项目组,然后项目组再根据项目进展情况做相应的操作。

能完成这件事情的函数叫epoll,它在内核中的实现不是通过轮询的方式,而是通过注册callback函数的方式,当某个文件描述符发送变化的时候,就会主动通知。

如图所示,假设进程打开了Socket m, n, x等多个文件描述符,现在需要通过epoll来监听是否这些Socket都有事件发生。其中epoll_create创建一个epoll对象,也是一个文件,也对应一个文件描述符,同样也对应着打开文件列表中的一项。在这项里面有一个红黑树,在红黑树里,要保存这个epoll要监听的所有Socket。

当epoll_ctl添加一个Socket的时候,其实是加入这个红黑树,同时红黑树里面的节点指向一个结构,将这个结构挂在被监听的Socket的事件列表中。当一个Socket来了一个事件的时候,可以从这个列表中得到epoll对象,并调用call back通知它。

这种通知方式使得监听的Socket数据增加的时候,效率不会大幅度降低,能够同时监听的Socket的数目也非常的多了。上限就为系统定义的、进程打开的最大文件描述符个数。因而,epoll被称为解决C10K问题的利器。

总结

写一个能够支撑大量连接的高并发的服务端不容易,需要多进程、多线程,而epoll机制能解决C10K问题。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/08/10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
[操作系统] 进程等待
进程等待(Process Waiting)是操作系统中父进程用于管理和同步子进程的重要机制。根据你的图片内容,我们可以分几个部分来讲解进程等待的相关知识。
DevKevin
2025/02/12
770
[操作系统] 进程等待
《Linux操作系统编程》 第六章 Linux中的进程监控: fork函数的使用,以及父子进程间的关系,掌握exec系列函数
使学生理解Linux中进程控制块的数据结构,Linux进程的创建、执行、终止、等待以及监控方法。并重点掌握fork函数的使用以及exec系列函数。
猫头虎
2024/04/08
2010
《Linux操作系统编程》 第六章 Linux中的进程监控: fork函数的使用,以及父子进程间的关系,掌握exec系列函数
Linux僵尸进程
版权声明:本文为博主原创文章,转载请注明博客地址: https://blog.csdn.net/zy010101/article/details/83715103
zy010101
2019/05/25
4.5K0
L010Linux和androidNDK之linux避免僵尸进程,子进程退出的处理
L010Linux和androidNDK之linux避免僵尸进程,子进程退出的处理
上善若水.夏
2018/09/28
3.2K0
和老李一起搞山寨Workerman(三)
今日清晨睡梦中醒来,已是7点40分有余。静躺在床上,耳边远处又断断续续萦绕着老人们在楼下的野园子里跟着于魁智吊嗓子的声音,时不时还夹杂着一段段抖空竹的风哨声。
老李秀
2019/12/11
1.1K0
和老李一起搞山寨Workerman(三)
【Linux信号】四:SIGCHLD信号
实际上,在子进程结束的时候,会产生一个SIGCHLD信号,信号描述如下,根据man手册可以知道,子进程结束运行,其父进程会收到SIGCHLD信号,该信号的默认处理动作是忽略。
mindtechnist
2024/08/08
2150
【Linux信号】四:SIGCHLD信号
【Linux探索学习】第十八弹——进程等待:深入解析操作系统中的进程等待机制
Linux学习笔记:https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482
GG Bond1
2024/12/04
1600
【Linux探索学习】第十八弹——进程等待:深入解析操作系统中的进程等待机制
【Linux进程控制】五、wait()函数——子进程回收
僵尸进程:子进程结束,父进程没有回收子进程的资源(PCB),这个资源必须要由父进程回收,否则就形成僵尸进程。
mindtechnist
2024/08/08
1170
【Linux进程控制】五、wait()函数——子进程回收
linux系统编程之进程(四):wait/waitpid函数与僵尸进程、fork 2 times
该文章介绍了如何在Linux系统中通过fork函数创建子进程,并详细讲解了fork函数的工作原理、父进程和子进程之间的通信以及fork函数引发的孤儿进程和僵尸进程等问题。同时,文章还介绍了如何使用wait和waitpid函数等待子进程结束,以及如何使用exec系列函数在子进程中执行新的程序。
s1mba
2018/01/03
3.4K0
僵尸进程的处理办法
在https://editor.csdn.net/md/?articleId=138925446这篇文章中,我缺失了关于僵尸进程的处理办法的内容,因为当时脑子不好的小菜鸟并未学到这里,现在就让我填上这个坑🕳吧😉
用户11039529
2024/05/24
720
僵尸进程的处理办法
OpenHarmony 内核源码分析(进程回收篇) | 进程关系链
进程是家族式管理的,父子关系,兄弟关系,朋友关系,子女关系,甚至陌生人关系(等待你消亡)在一个进程的生命周期中都会记录下来.用什么来记录呢?当然是内核最重要的胶水结构体LOS_DL_LIST,进程控制块(以下简称PCB)用了8个双向链表来记录进程家族的基因关系和运行时关系.如下:
小帅聊鸿蒙
2025/03/21
340
通过linux0.11理解僵尸进程
首先僵尸进程产生的原因是子进程退出了,但是父进程没有回收他的资源(pcb),所以我们从源头开始分析这个过程。那就是子进程退出的时候。进程是通过exit系统调用退出的。 我们看一下exit函数的代码。
theanarkh
2020/02/17
2K0
【Linux系统编程】七、进程等待
​ 之前我们讲过,子进程退出,父进程如果还在执行中,就会造成 “ 僵尸进程 ” 的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“ 杀人不眨眼 ” 的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
利刃大大
2025/02/18
870
【Linux系统编程】七、进程等待
Linux进程管理命令及状态详解
查看进程树。 linux中,每一个进程都是由其父进程创建的。此命令以可视化方式显示进程,通过显示进程的树状图来展示进程间关系。如果指定了pid了,那么树的根是该pid,不然将会是init(pid: 1)。
bboy枫亭
2020/09/22
1.9K0
Linux进程管理命令及状态详解
深入理解计算机系统:进程
导语:这是篇读书笔记,每次重读CSAPP都有新的认知,尤其是在进入了后台通道之后才感受到每天和进程打交道的感觉是如此深刻。 0x00 What is Process? [ system structure ] 进程(Process) 经典定义是一个执行中的程序的实例,操作系统对一个正在运行的程序的一种抽象。并发运行,指的是一个进程的指令和另一个进程的指令交错执行。操作系统实现这种交错执行的机制称为上下文切换。 线程(Thread) 一个进程可以由多个线程的执行单元组成,每个线程都运行在进程的
腾讯技术工程官方号
2019/09/07
1.3K0
深入理解计算机系统:进程
【linux】进程等待与进程替换
任何子进程,在退出的情况下,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,状态Z(僵尸状态),内存泄漏
用户11029103
2024/11/16
1100
【linux】进程等待与进程替换
Linux wait() 和 waitpid()函数介绍
转载自http://blog.csdn.net/wallwind/article/details/6998602 ---- 当一个进程正常或异常终止的时候,内核就像其父进程发送SIGCHLD信号,因为子进程是个一步事件,所以这种信号也是内核系那个父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数。对于这种信号的系统默认动作是忽略它。 现在要知道调用wait或waitpid的进程可能会发生什么情况:  如果其所有子进程都在运行,则阻塞。 如果一个子进程已经终止,正在得带的
老白
2018/03/19
2.5K0
Linux wait() 和 waitpid()函数介绍
僵尸进程
  在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等.但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination statu
猿人谷
2018/01/17
1.4K0
Linux进程控制——Linux进程等待
前言:接着前面进程终止,话不多说我们进入Linux进程等待的学习,如果你还不了解进程终止建议先了解:
Eternity._
2024/06/14
2280
Linux进程控制——Linux进程等待
Linux僵尸进程以及wait函数
僵尸进程就是已经结束的进程(几乎不占计算机资源),但是它并没有从进程列表中删除。僵尸进程太多会导致操作系统的进程数目过多,从而占满了OS的进程表。进而导致无法创建新进程,致使OS崩溃。
zy010101
2022/05/05
1.8K0
Linux僵尸进程以及wait函数
推荐阅读
相关推荐
[操作系统] 进程等待
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文