/favicon.ico

子恒的博客

[杂谈]我怎么选择笔记

我怎么选择笔记 折腾了这么久笔记软件,终于有了一个最终的方案,给大家分享下 用过的软件对比 软件 优点 缺点 用途 印象笔记 云/多端 丑,难用,残疾md 手机查看 OneNote 多目录/手写 同步难,没有md 读书、网课笔记 Typora 好看/导出 没有云、收费 离线编辑 马克飞象 比较好看 没有云、收费 离线编辑 Obsdian 比较好看 没有云、不能导出 离线编辑 CmdMarkdown 有云 难看 备份,网页版查看 StackEdit 简单 太卡了 没用 MarginNote 知识图谱 无win 读pdf书 备忘录 多端同步、轻量 格式难用 速记 Notion 多端同步、复杂 md不好用,手写不支持 替代onenote 诉求 支持markdown,且美观 支持Latex,脚注 支持云存储,移动端同步 支持mac/windows/ios/网页版 支持3级目录,支持搜索 支持手写插入 支持网页剪裁 支持离线编辑 可以导出pdf,html 支持代码块,代码块染色,行号 支持大纲 工作流 没有一个通用的工作流,所以我需要根据不同的场景来决定采用哪些软件。 速记 (todo list,check list) 备忘录 写博客 Obsdian -> github -> hugo

[后台]服务端高性能架构之道(系统和服务篇)

如果你在服务端的工区,常常会听到同学们激烈的讨论,包括能不能扛得住xx流量?能不能P99达到x毫秒?某操作能不能立即生效?某服务CPU飙升了,某服务OOM了,某服务超时率暴涨了? 这些灵魂的质问,其实就是在保障服务端的高并发、高性能、高可用、高一致性等等,是我们服务端同学必备的扎实基本功。 克服系统瓶颈 服务端的代码都跑在各种版本的Linux之上,所以高性能的第一步要和操作系统打交道。我们的服务需要通过操作系统进行I/O、CPU、内存等等设备的使用,同时在使用各种系统调用时避免各种资源的开销过大。 零拷贝 认识零拷贝之前,我们先要对Linux系统I/O机制有一定的了解。当我们执行一个write(2)或者read(2)的时候(或者recv和send),什么时候操作系统会执行读写操作?什么时候又最终会落到磁盘上? 以一个简单的echo服务器为例,我们模拟下每天都在发生的请求和回包: 1 2 3 4 5 6 sockfd = socket(...); //打开socket buffer = new buffer(...); //创建buffer while((clientfd = accept(socketfd...)){ // 接收一个请求 read(clientfd, buffer, ...); //从文件内容读到buffer中 write(clientfd, buffer, ...); //将buffer中的内容发送到网络 } 看一下这段代码的拷贝流程(下图): 数据包到达网卡,网卡进行DMA操作,把网卡寄存器的数据拷贝到内核缓冲区 CPU把内核缓冲区的数据拷贝到用户空间的缓冲区 用户空间处理buffer中的数据(此处不处理) CPU把用户空间的缓冲区的数据拷贝到内核缓冲区 网卡进行DMA操作,把内核缓冲区的数据拷贝到网卡寄存器,发送出去 整个过程触发了4次拷贝(2次CPU,2次DMA),2次系统调用(对应4次上下文切换) (注:DMA(Direct Memory Access), I/O 设备直接访问内存的一个通道,可以完成数据拷贝,使得CPU 不再参与任何拷贝相关的事情,现在几乎所有的设备都有DMA) 使用mmap mmap可以把用户空间的内存地址映射到内核空间,这样对用户空间的数据操作可以反映到内核空间,省去了用户空间的一次拷贝: 应用调用mmap,和内核共享缓冲区(只需一次) 数据包到达网卡,网卡进行DMA操作,把网卡寄存器的数据拷贝到内核缓冲区 CPU把接收到的内核缓冲区的数据拷贝到发送的内核缓冲区 网卡进行DMA操作,把内核缓冲区的数据拷贝到网卡寄存器,发送出去 整个过程触发了3次拷贝(1次CPU,2次DMA),2次系统调用(对应4次上下文切换) 使用sendfile/splice Linux 内核版本 2.1 中实现了一个函数sendfile(2): 他把read(2)和write(2)合二为一,成为一次系统调用,实现了把一个文件读取并写到另一个文件的语义 系统调用中不再切换回用户态,而是在内核空间中直接把数据拷贝过去(2.4 之后这一步支持了DMA拷贝,实现了CPU零拷贝) 我门看下使用sendfile之后的流程: 整个过程触发了3次拷贝(0次CPU,3次DMA),1次系统调用(对应2次上下文切换) Linux 内核版本 2.6 中实现了一个函数splice(2),类似sendfile,但是接收/发送方必须有一个文件是管道,通过管道的方式连接发送方和接收方的内核缓冲区,不再需要拷贝(0次CPU,2次DMA,1次系统调用)

[Go]Go语言的设计和坑

本文介绍了Go的语言设计和一些容易踩坑的细节: 理解Go为什么X,摆脱原语言的思维 解决写代码时比较困惑和不满的点,对容易出错的语法有个印象 Go学起来非常简单,但是这是语言设计者刻意为之,很多复杂的细节都藏在语言实现里,导致我们迅速学会Go之后不断踩坑 Why Go 2007年,Google设计Go,目的在于提高在并行编程(多核CPU越来越多)、分布式部署、大型代码库(以及维护他们的非常多的开发人员)的情况下的开发效率。设计时,在吸收C++优点的基础上,收集于很多工程师之间流传的的“不要像C++” Go like C++: 内存消耗少 执行速度快 启动快 Go not like C++: 程序编译时间短(按照我过去的经验,一个C++大型项目即使make -j8也需要编译一个小时以上) 像动态语言一样灵活(runtime、interface、闭包、反射) 内置并发支持(C++的协程至少得等到std23才有,非常落后) 丰富的原生库(C++解析json,建立http服务器,使用redis这种都很难找到靠谱的库) 多语义(取消了指针运算、取消隐式类型转换、取消类型别名,取消重载,++和赋值作为表达式…) Go的优点: 面向工程:简单。只有25个关键字,代码风格统一,可读性高,go mod包丰富 自动垃圾回收:语言运行时内置垃圾回收 语言级并发:非常好用的routine和channel,更高层次的并发抽象 静态语言动态特性 Go的缺点: runtime的性能还需要提高 没有泛型 冗余的错误处理 Go mod不够完善 Go语⾔将⾛向何⽅? 我为什么放弃Go语言 Go的设计哲学 创始人Rob Pike在SPLASH上的演讲,阐述了设计Go的初衷 许式伟,Go和Java在继承观念上的对比 对面向对象的批评 王垠:解密“设计模式”,对设计模式的批评 少即是多(less is more):如果一个特性并不对解决任何问题有显著价值,那么go就不提供它;如果需要一个特性,那么只有一种方法去实现 面向接口编程:非侵入式接口,反对继承、反对虚函数和虚函数重载(多态)、删除构造和析构函数 正交+组合的语言特性:语言的特性之间相互独立,不相互影响。比如类型和方法是互相独立的,类型之间也是相互独立的,没有子类,包也没有子包。不同特性用组合的方式来松耦合 并发在语言层面支持:并发更好利用多核,有更强的表现力来模拟真实世界 在设计上,Go秉承了C的简单粗暴。 为什么没有继承? Go没有子类型的概念,只能把类型嵌入到另一个类型中,所以没有类型系统。Go的作者认为类型系统被过度使用了,应该在这个方向上退一步。 使用伸缩性良好的组合,而不是继承 数据和方法不再绑定在一起,数据的集合用struct,方法的集合用interface,保持正交 类似子类父类的系统造成非常脆弱的代码。类型的层次必须在早期进行设计,通常会是程序设计的第一步,但是一旦写出程序后,早期的决策就很难进行改变了。所以,类型层次结构会促成早期的过度设计,因为程序员要尽力对软件可能需要的各种可能的用法进行预测,不断地为了避免挂一漏万,不断的增加类型和抽象的层次。这种做法有点颠倒了,系统各个部分之间交互的方式本应该随着系统的发展而做出相应的改变,而不应该在一开始就固定下来。 作者附了一个例子,是一些以接口为参数并且其返回结果也是一个接口的函数: 1 2 3 4 5 6 // 入参是接口的函数,而不是成员方法 func ReadAll(r io.Reader) ([]byte, error) // 封装器 - 出入参都是接口 func LoggingReader(r io.

[Redis]Redis 应用篇

分布式锁 简单加锁 1 2 3 4 5 // 思想:利用setnx检测有没有set过,如果set过就表示没有抢到锁 > setnx locker true OK // ... do somthing ... > del locker 处理set之后进程崩溃的死锁问题 1 2 3 4 5 6 // 思想:给锁加上过期时间,即使set之后进程挂掉,也不会死锁 > setnx locker true OK > expire locker 5 // ... do somthing ... > del locker 处理非原子性问题 setnx之后,expire之前,进程挂了,也会死锁。怎么处理这种情况? 使用redis事务吗?事务里没有if else,要么全部执行,要么全部不执行。需求是setnx成功才执行expire,有依赖关系,没法用事务 使用新的原子命令,如下 1 2 3 4 > set locker true ex5 nx OK // ... do somthing ... > del locker 处理超时问题 上面的方案设定了超时时间。但是如果少数操作的时间超过了超时时间怎么办?有两个问题:

[C++]C++ 11/14 笔记(四)

本部分介绍了c++11的并发编程。这些笔记是未完成的。 语法参考这里: 现代C++教程 实现参考这里: C++11中的mutex, lock,condition variable实现分析 std::thread C++ 11为我们带来了语言级的线程支持,包括线程的创建和等待: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //g++ -o t main.cpp -lpthread --std=c++11 #include <iostream> #include <thread> #include <chrono> void foo(int sec) { // sleep_for() 休眠sec秒后唤醒 std::this_thread::sleep_for(std::chrono::seconds(sec)); } int main() { // join() 阻塞等待线程 // joinable() 是否可以join(没设置任务函数不可以join) std::thread t; std::cout << "before starting, joinable: " << t.

[后台]负载均衡 (三)限流篇

限流 限流能力是高并发系统中,对于服务提供方的一种保护手段。通过限流功能,我们可以通过控制QPS的方式,以避免被瞬时的流量高峰冲垮,从而保障系统的高可用性。 考虑的问题 完成一个限流系统, 我们可以结合场景的需要做下面的考虑 多规则匹配:是否会存在有多重规则的限流?比如有的规则限制每天1000次,有的规则限制每分钟1次?是同时生效还是优先生效某个? 资源类型:能限流什么?QPS,连接数,并发数 全局限流/单机限流:多个服务的实例共享一个全局的流量限额,比如所有机器共享1000QPS。或者单个实例的限流,比如被调限定每台机器不超过1000QPS 限流阈值:单位时间内的最大配额数。是按照每秒种一次,还是按照每分钟60次? 限流处理:客户端如何处理超出限额的请求?超额后直接拒绝,还是超额后进行排队? 抽象出一个方案 接口级别限流:每个接口分配一个appid和key,各自计算各自的配额 多维度限流:支持每秒N次、每分钟N次、每天N次等维度 匀速防刷:假设配置了每分钟60次,依然可能出现第一秒访问了60次用光了配额。匀速防刷可以匀速消耗配额,解决这个问题 多级限流:支持不同的限流规则,并有采用的优先级,采用优先级最高的方案进行限流 限流算法 固定窗口 固定窗口是在一段时间内可以限制访问次数的方法。 将时间划分为多个窗口 在每个窗口内每有一次请求就将计数器加一 如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃。当时间到达下一个窗口时,计数器重置 这样有一定的限流效果,但是限制住的流量可能是有毛刺的。比如1000次/分钟,可能00:59的时候有1000流量,01:00的时候也有1000流量,这样这两秒内就有2000流量! 具体实现:用一个变量C标记访问次数,一个事件定时过期,并在过期时把变量C清零: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type FixedWindowCounter struct { TimeSlice time.Duration NowCount int32 AllowCount int32 } func (p *FixedWindowCounter) Take() bool { once.Do(func() { go func() { for { select { case <-time.

[后台]负载均衡(二)能力篇

名字服务 基础设计 名字服务考虑的基本设计 客户端发现: 服务提供者的实例在启动时或者位置信息发生变化时会向服务注册表注册自身,在停止时会向服务注册表注销自身,如果服务提供者的实例发生故障,在一段时间内不发送心跳之后,也会被服务注册表注销。 服务消费者的实例会向服务注册表查询服务提供者的位置信息,然后通过这些位置信息直接向服务提供者发起请求。 服务端发现: 第一步与客户端发现相同。 服务消费者不直接向服务注册表查询,也不直接向服务提供者发起请求,而是将对服务提供者的请求发往一个中央路由器或者负载均衡器,中央路由器或者负载均衡器查询服务注册表获取服务提供者的位置信息,并将请求转发给服务提供者。 这两种模式各有利弊,客户端发现模式的优势是,服务消费者向服务提供者发起请求时比服务端发现模式少了一次网络跳转,劣势是服务消费者需要内置特定的服务发现客户端和服务发现逻辑; 服务端发现模式的优势是服务消费者无需内置特定的服务发现客户端和服务发现逻辑,劣势是多了一次网络跳转,并且需要基础设施环境提供中央路由机制或者负载均衡机制。目前客户端发现模式应用的多一些,因为这种模式的对基础设施环境没有特殊的要求,和基础设施环境也没有过多的耦合性。 主调调用被调时,根据被调的名字从服务注册中心获取服务实例列表,包括节点ip、端口、权重、地理位置等;一般采取分钟级别的定时任务去拉取,本地做缓存,异步更新。 实现方式 DNS,传播速度太慢,没法发现端口。SkyDNS解决了这个问题,在k8s里大量使用 zookeeper或者etcd,如SmartStack,能保证强一致,但是要做很多开发 Eureka。Netflix的java生态里的优秀方案 Consul,提供服务配置、服务的内存和磁盘监测等 服务注册信息 IP和端口 一个服务端要接入名字服务,必须要先提供自己的IP和端口信息。 IP的获取方法: 通过遍历网卡的方式去获取,找到第一个不为本地环回地址的 IP 地址。dubbo就是这种方法 指定网卡名interfaceName,来获取IP 直接与服务注册中心建立 socket 连接,然后通过socket.getLocalAddress() 这种方式来获取本机的 IP 端口的获取方法: 一般的RPC服务或者Web服务监听的端口都在配置中写好,可以直接获取上报。 扩展设计 除了IP和端口,可能还有一些常用的服务信息需要注册上来,提供更高级的功能: 1.支持TLS:想知道某个 HTTP 服务是否开启了 TLS。 2.权重:对相同服务下的不同节点设置不同的权重,进行流量调度。 3.环境分配:将服务分成预发环境和生产环境,方便进行AB Test功能。 4.机房:不同机房的服务注册时加上机房的标签,以实现同机房优先的路由规则。 无损注册/下线 虽然服务注册一般发生在服务的启动阶段,但是细分的话,服务注册应该在服务已经完全启动成功,并准备对外提供服务之后才能进行注册。 1.有些 RPC 框架自身提供了方法来判断服务是否已经启动完成,如 Thrift ,我们可以通过 Server.isServing() 来判断。 2.有一些 RPC 框架本身没有提供服务是否启动完成的方式,这时我们可以通过检测端口是否已经处于监听状态来判断。 3.而对于 HTTP 服务,服务是否启动完毕也可以通过端口是否处于监听状态来判断。 下线也是一样的,可以注册服务下线的回调,或者监听服务下线的信号,或者做健康检查 健康检查 客户端主动心跳上报健康: 客户端每隔一定时间主动发送“心跳”的方式来向服务端表明自己的服务状态正常,心跳可以是 TCP 的形式,也可以是 HTTP 的形式。 也可以通过维持客户端和服务端的一个 socket 长连接自己实现一个客户端心跳的方式。 客户端的健康检查只能表明网络可达,不能代表服务可用。服务端的健康检查可以准确获得服务的健康状态: 服务端调用服务发布者某个 HTTP 接口来完成健康检查。 对于没有提供 HTTP 服务的 RPC 应用,服务端调用服务发布者的接口来完成健康检查。 可以通过执行某个脚本的形式来进行综合检查,覆盖多个场景。

[后台]负载均衡 (一)算法篇

当单机的访问压力很大时,就需要引入集群。集群一个很重要的事情就是把请求均匀地分配在各个机器上,这就是负载均衡的雏形。 有基于MAC地址的二层负载均衡和基于IP地址的三层负载均衡。 二层负载均衡会通过一个虚拟MAC地址接收请求,然后再分配到真实的MAC地址;三层负载均衡会通过一个虚拟IP地址接收请求,然后再分配到真实的IP地址; 四层通过虚拟IP+端口接收请求,然后再分配到真实的服务器(比如LVS,F5);七层通过虚拟的URL或主机名接收请求,然后再分配到真实的服务器(Haproxy和Nginx)。 四层和七层是最常见的负载均衡模型。 **四层:**以常见的TCP为例,负载均衡设备在接收到第一个来自客户端的SYN请求时,通过负载均衡算法选择服务器,并对报文中目标IP地址进行修改(改为后端服务器IP),直接转发给该服务器。TCP的连接建立,即三次握手是客户端和服务器直接建立的,负载均衡设备只是起到一个类似路由器的转发动作。在某些部署情况下,为保证服务器回包可以正确返回给负载均衡设备,在转发报文的同时可能还会对报文原来的源地址进行修改。 **七层:**以常见的TCP为例,负载均衡设备如果要根据真正的应用层内容再选择服务器,只能先代理最终的服务器和客户端建立连接(三次握手)后,才可能接受到客户端发送的真正应用层内容的报文,然后再根据该报文中的特定字段,再加上设置的负载均衡算法,选择内部某台服务器。负载均衡设备在这种情况下,更类似于一个代理服务器。负载均衡和前端的客户端以及后端的服务器会分别建立TCP连接。所以从这个技术原理上来看,七层负载均衡明显的对负载均衡设备的要求更高,处理七层的能力也必然会低于四层模式的部署方式。 参考资料:四层和七层负载均衡的区别 nginx用的负载均衡算法 Nginx可以作为HTTP反向代理,把访问本机的HTTP请求,均分到后端集群的若干台服务器上。负载均衡的核心就是负载均衡所使用的平衡算法,适用于各种场景。 Nginx的负载均衡算法 Nginx目前提供的负载均衡模块: ngx_http_upstream_round_robin,加权轮询,可均分请求,是默认的HTTP负载均衡算法,集成在框架中。 ngx_http_upstream_ip_hash_module,IP哈希,可保持会话。 ngx_http_upstream_least_conn_module,最少连接数,可均分连接。适用于链接数体现资源的服务,比如FTP。 ngx_http_upstream_hash_module,一致性哈希,可减少缓存数据的失效。 随机访问 在介绍nginx的模式前,先介绍下普通的负载均衡方法。假设有7个请求,我们给A、B、C三个节点分别4、2、1的权重。最朴素的负载均衡方式有下面几种: 完全轮询:访问完A去访问B,访问完B去访问C,再去访问A。缺点是没有权重,不能根据负载调节。 列表轮询:构造一个数组[A, A, A, A, B, B, C],每次pop出去一个访问。缺点是pop出去的元素太随机,可能一次集中访问A ,而且占用内存太大,对于几万的权重范围不合适。 随机数:我们按照A、B、C的权重划分好区间,A(0、1、2、3),B(4、5),C(6),然后取一个随机数,模余7,看看最后的结果在哪个区间内,就取哪个节点。缺点是完全随机,无法避免集中访问。 加权轮询 假设有7个请求,我们给A、B、C三个节点分别4、2、1的权重。如果随机按照概率来选,那么很可能出现连续四个请求都在A上面的情况,这样只能保证结果看起来均衡,但是时间段内不均衡。Nginx采用了一种平滑的加权平均算法来选取节点(Weighted Round Robin)。 先引入三个概念,都用来描述服务器节点的权重: $W$ : weight 我们指定的权重,就是上面例子中的4、2、1。 $W_{ew}$: effective_weight 有效权重,初始值为$W$。用来对故障节点降权。 如果通信中有错误产生,就减小effective_weight。(故障降权) 此后有新的请求过来时,再逐步增加effective_weight,最终又恢复到weight。(自动恢复) $W_{cw}$ : current_weight 当前真实权重,每次都会选到最大的真实权重的节点去请求 真实权重$W_{cw}$计算方式: 初始化:$W_{cw}$ 起始值为0 获得实时权重:请求到来后,给每个节点的真实权重加上有效权重,即$每个节点 W_{cw} = W_{cw} + W_{ew}$ 选出最大权重:选择真实权重最大的节点最为本次请求的目标 回避刚选的节点:最选择的节点的实时权重减去所有节点(包括自己)的有效权重和。即$选中节点 W_{cw} = W_{cw} - (W_{ew1} + W_{ew2} + … + W_{ewn})$ 来看一个具体的例子: 假设A、B、C三个节点的权重分别为4、2、1。

[Linux]文件和零拷贝

文件 文件描述符 文件描述符:在Linux中,所有的文件都是通过文件描述符引用。fd是一个非负整数。按照惯例,标准输入的fd是0,标准输出的fd是1,标准错误的fd是2。分别作为STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO定义在unistd中。 文件描述符的上限:fd的范围是 0 ~ OEPN_MAX-1 。OPEN_MAX一般是20或者64。这代表一个进程最多打开19或63个文件。 文件内核API 文件的打开:int open(const char *pathname, int flags)参数填上要打开的文件的名字(甚至可以不存在),会返回打开的fd。下面是一些常用的选项: 1 2 3 4 5 6 O_APPEND 文件将以追加模式打开,每次写操作之前,文件偏移量都会置于文件末尾。 O_CREAT 创建文件。如果文件已经存在,则会直接打开。 O_EXCL 和上面的O_CREAT联用时,表示如果文件已经存在,就会失败。可以保证多进程同时创建文件的原子操作。 O_SYNC 打开文件用于同步I/O。在数据写到磁盘之前写操作不会完成;一般的读操作已是同步的,所以这个标志对读操作没有影响。 O_NONBLOCK 如果可以,文件将在非阻塞模式下打开。任何其它操作都不会使该进程在I/O中阻塞。这种情况可能只用于FIFO。 O_DIRECT 打开文件用于直接I/O。将会绕过缓冲区操作。 文件的关闭:int close(int fd) 关闭一个文件会释放上面所有的记录锁。一个进程终止后,内核会自动关闭它打开的所有文件。 文件定位: off_t lseek(int fd, off_t offset, int whence) 参数whence指定了偏移地址(开始点SEEK_SET 当前点SEEK_CUR 结束点SEET_END),另一个参数offset是从参考点开始的偏移量(可正可负)。返回新的偏移地址。 空洞文件:如果写入一部分之后lseek到后面去写入,中间就会产生一个空洞,实际不占用磁盘大小。 文件读取:ssize_t read(int fd, void *buf, size_t nbytes) 返回读取到的字节数。下面的情况可能使得读取到的字节数少于需要的字节数: 1)再读这么多就到了文件尾 2)读网络缓冲区读完 3)读FIFO管道包含的字节少于需要的长度 4)读终端设备,一次一行 文件写入:ssize_t write(int fd, const void* buf, size_t nbytes) 返回写入的字节数。一般和nbytes相同。 文件属性编辑:int fcntl(int fd, int cmd, .

[Linux]Linux socket API

创建和关闭 int socket(int domain, int type, int protocol) 创建一个socket。 domain 指定了通信的特性。AF_UNIX Unix域,AF_INET IPv4域。 AF_INET6 IPv6域。 type 指定了连接的类型。 SOCK_STREAM 有序、可靠、双向、面向连接的字节流 TCP SOCK_DGRAM 不可靠、无连接、固定长度的报文 UDP SOCK_SEQPACKET 固定长度、有序、可靠、双向、面向连接的字节流 protocol 为0时,选择指定域的默认协议。AF_INET域的SOCK_STREAM默认的协议为TCP,SOCK_DGRAM则为UDP。还有IPPROTO_ICMP、IPPROTO_IP、IPPROTO_RAW等。SOCK_STREAM提供字节流服务,所以程序分不出报文的界限。 int shutdown(int socketfd, int how) 关闭一个socket。 套接字函数是双向的。可以用这个函数关闭套接字的某个方向的IO。 how是SHUT_RD表示关闭读端,无法从套接字读取数据。SHUT_WR是关闭写端,无法向套接字写入数据。SHUT_RDWR无法读也无法写。 close()是一个fd的通用释放函数。他和shutdown有何不同?1) close要等所有的活动引用关闭后才释放套接字。这使得dup之后的套接字必须等待所有的引用释放。但是shutdown和引用fd数量无关。2)shutdown可以方便地关闭读写的任何一端。 网络地址 地址的结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // Linux 套接字 // 这是Linux定义的一个通用结构。所有地址都可以强转为这个结构体,比如IPV4、IPV6等,便于使用。 struct sockaddr { u_short sa_family; /* address family */ char sa_data[14]; /* up to 14 bytes of direct address */ }; // IPv4 地址 // 1.