我相信大家刚开始学socket的时候,都跟我一样。云里雾里的,对socket的概念很模糊。这篇文章我打算从一个初学者的角度开始聊起,让大家了解下我眼里的socket是什么以及socket的原理和内核实现。
插头与插座当我将插头插入插座,那看起来就像是将两者连起来了。
风扇与电力系统建立"连接"而插座的英文,又叫socket。巧了,我们程序员搞网络编程时也会用到一个叫socket的东西。其实两者非常相似。通过socket,我们可以与某台机子建立"连接",建立"连接"的过程,就像是将插口插入插槽一样。
大概概念是了解了,但我相信各位对socket其实还是很模糊。我们从大家最熟悉的使用场景开始说起。
TCP是什么那这时候就需要用socket进行编程。于是第一步就是创建个关于TCP的socket。就像下面这样。sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
握手建立连接流程连接建立完成后,客户端可以执行send() 方法发送消息,服务端可以执行recv()方法接收消息,反过来,服务器也可以执行send(),客户端执行recv()方法。到这里为止,就是我们大部分程序员最熟悉的使用场景。
读写收发但显然,事情没那么简单。这里还有两个问题。第一个是,接收端和发送端可能不止一个,因此我们需要一些信息做下区分,这个大家肯定很熟悉,可以用IP和端口。IP用来定位是哪台电脑,端口用来定位是这台电脑上的哪个进程。第二个是,发送端和接收端的传输方式有很多区别,可以是可靠的TCP协议,也可以是不可靠的UDP协议,甚至还需要支持基于icmp协议的ping命令。
sock加入IP和端口字段而第二个问题,我们会发现这些协议虽然各不相同,但还是有一些功能相似的地方,比如收发数据时的一些逻辑完全可以复用。按面向对象编程的思想,我们可以将不同的协议当成是不同的对象类(或结构体),将公共的部分提取出来,通过"继承"的方式,复用功能。
继承sock的各类socksock是最基础的结构,维护一些任何协议都有可能会用到的收发数据缓冲区。inet_sock特指用了网络传输功能的sock,在sock的基础上还加入了TTL,端口,IP地址这些跟网络传输相关的字段信息。说到这里大家就懵了,难道还有不是用网络传输的?有,比如Unix domain socket,用于本机进程之间的通信,直接读写文件,不需要经过网络协议栈。这是个非常有用的东西,我以后一定讲讲(画饼)。inet_connection_sock 是指面向连接的sock,在inet_sock的基础上加入面向连接的协议里相关字段,比如accept队列,数据包分片大小,握手失败重试次数等。虽然我们现在提到面向连接的协议就是指TCP,但设计上linux需要支持扩展其他面向连接的新协议,tcp_sock 就是正儿八经的tcp协议专用的sock结构了,在inet_connection_sock基础上还加入了tcp特有的滑动窗口、拥塞避免等功能。同样udp协议也会有一个专用的数据结构,叫udp_sock。好了,现在有了这套数据结构,我们将它们跟硬件网卡对接一下,就实现了网络传输的功能。这个文件句柄fd其实就是 sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) 里的sock_fd。将句柄暴露给用户,之后用户就可以像操作文件句柄那样去操作这个sock句柄。在用户空间里操作这个句柄,文件系统就会将操作指向内核sock结构。是的,操作这个特殊的文件就相当于操作内核里对应的sock。
通过文件找到sock有了sock_fd句柄之后,我们就需要提供一些接口方法,让用户更方便的实现特定的网络编程功能。这些接口,我们列了一下,发现需要有send(),recv(),bind(), listen(),connect()这些。到这里,我们的内核网络传输功能就算设计完成了。现在是不是眼熟了,上面这些接口方法其实就是socket提供出来的接口。所以说,socket其实就是个代码库 or 接口层,它介于内核和应用程序之间,提供了一些高度封装过的接口,让我们去使用内核网络传输功能。
基于sock实现网络传输功能到这里,我们应该明白了。我们平时写的应用程序里代码里虽然用了socket实现了收发数据包的功能,但其实真正执行网络通信功能的,不是应用程序,而是linux内核。相当于应用程序通过socket提供的接口,将网络传输的这部分工作外包给了linux内核。这听起来像不像我们最熟悉的前后端分离的服务架构,虽然这么说不太严谨,但看上去linux就像是被分成了应用程序和内核两个服务。内核就像是后端,暴露了好多个api接口,其中一类就是socket的send()和recv()这些方法。应用程序就像是前端,负责调用内核提供的接口来实现想要的功能。
进程通过socket调用内核功能看到这里,我担心大家会有点混乱,来做个小的总结。在操作系统内核空间里,实现网络传输功能的结构是sock,基于不同的协议和应用场景,会被泛化为各种类型的xx_sock,它们结合硬件,共同实现了网络传输功能。为了将这部分功能暴露给用户空间的应用程序使用,于是引入了socket层,同时将sock嵌入到文件系统的框架里,sock就变成了一个特殊的文件,用户就可以在用户空间使用文件句柄,也就是socket_fd来操作内核sock的网络传输能力。这个socket_fd是一个int类型的数字。现在回去看socket的中文翻译,套接字,我将它理解为一套用于连接的数字,是不是就觉得特别合理了。
网络分层与基于sock实现网络传输功能
TCP三次握手在服务端握手次数还没达到"三次"的连接,叫半连接,完成好三次握手的连接,叫全连接。它们分别会用半连接队列和全连接队列来存放,这两个队列会在你执行listen()方法的时候创建好。当服务端执行accept()方法时,就会从全连接队列里拿出一条全连接。
半连接队列和全连接队列至此,连接就算准备好了,之后,就可以开始传输数据。虽然都叫队列,但半连接队列其实是个hash表,而全连接队列其实是个链表。那么问题来了,为什么半连接队列要设计成哈希表而全连接队列是个链表?这个在我在我之前写的《没有accept,能建立TCP连接吗?》 已经提到过,不再重复。
sock的发送和接收缓冲区IP和端口其实不在sock下,而在inet_sock下,上面这么画只是为了简化。。。那么问题来了,发送数据是应用程序主动发起,这个大家都没问题。那接收数据呢?数据从远端发过来了,怎么通知并给到应用程序呢?这就需要用到等待队列。
sock内的等待队列当你的应用进程执行recv()方法尝试获取(阻塞场景下)接收缓冲区的数据时。
recv时无数据进程进入等待队列有时候,你会看到多个进程通过fork的方式,listen了同一个socket_fd。在内核,它们都是同一个sock,多个进程执行listen()之后,都嗷嗷等待连接进来,所以都会将自身的进程信息注册到这个socket_fd对应的内核sock的等待队列中。如果这时真来了一个连接,是该唤醒等待队列里的哪个进程来接收连接呢?这个问题的答案比较有趣。
惊群效应看到这里,问题又来了。服务端 listen 的时候,那么多数据到一个 socket 怎么区分多个客户端的?以TCP为例,服务端执行listen方法后,会等待客户端发送数据来。客户端发来的数据包上会有源IP地址和端口,以及目的IP地址和端口,这四个元素构成一个四元组,可以用于唯一标记一个客户端。其实说四元组并不严谨,因为过程中还有很多其他信息,也可以说是五元组。。。但大概理解就好,就这样吧。。。
四元组服务端会创建一个新的内核sock,并用四元组生成一个hash key,将它放入到一个hash表中。
四元组映射成hash键下次再有消息进来的时候,通过消息自带的四元组生成hash key再到这个hash表里重新取出对应的sock就好了。所以说服务端是通过四元组来区分多个客户端的。
多个hash_key对应多个客户端struct tcp_sock {
/* inet_connection_sock has to be the first member of tcp_sock */
struct inet_connection_sock inet_conn;
// 其他字段
}
struct inet_connection_sock {
/* inet_sock has to be the first member! */
struct inet_sock icsk_inet;
// 其他字段
}// sock 转为 tcp_sock
static inline struct tcp_sock *tcp_sk(const struct sock *sk)
{
return (struct tcp_sock *)sk;
}
内存布局类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返
它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput
我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?