45 Socket 源码及面试题

引导语

Socket 中文翻译叫套接字,可能很多工作四五年的同学都没有用过这个 API,但只要用到这个 API 时,必然是在重要的工程的核心代码处。

大家平时基本都在用开源的各种 rpc 框架,比如说 Dubbo、gRPC、Spring Cloud 等等,很少需要手写网络调用,以下三小节可以帮助大家补充这块的内容,当你真正需要的时候,可以作为手册示例。

本文和《ServerSocket 源码及面试题》一文主要说 Socket 和 ServerSocket 的源码,《工作实战:Socket 结合线程池的使用》这章主要说两个 API 在实际工作中如何落地。

1 Socket 整体结构

Socket 的结构非常简单,Socket 就像一个壳一样,将套接字初始化、创建连接等各种操作包装了一下,其底层实现都是 SocketImpl 实现的,Socket 本身的业务逻辑非常简单。

Socket 的属性不多,有套接字的状态,SocketImpl,读写的状态等等,源码如下图:

套接字的状态变更都是有对应操作方法的,比如套接字新建(createImpl 方法)后,状态就会更改成 created = true,连接(connect)之后,状态更改成 connected = true 等等。

2 初始化

Socket 的构造器比较多,可以分成两大类:

  1. 指定代理类型(Proxy)创建套节点,一共有三种类型为:DIRECT(直连)、HTTP(HTTP、FTP 高级协议的代理)、SOCKS(SOCKS 代理),三种不同的代码方式对应的 SocketImpl 不同,分别是:PlainSocketImpl、HttpConnectSocketImpl、SocksSocketImpl,除了类型之外 Proxy 还指定了地址和端口;
  2. 默认 SocksSocketImpl 创建,并且需要在构造器中传入地址和端口,源码如下:
// address 代表IP地址,port 表示套接字的端口
// address 我们一般使用 InetSocketAddress,InetSocketAddress 有 ip+port、域名+port、InetAddress 等初始化方式
public Socket(InetAddress address, int port) throws IOException {this(address != null ? new InetSocketAddress(address, port) : null,(SocketAddress) null, true);
}

这里的 address 可以是 ip 地址或者域名,比如说 127.0.0.1 或者 www.wenhe.com。

我们一起看一下这个构造器调用的 this 底层构造器的源码:

// stream 为 true 时,表示为stream socket 流套接字,使用 TCP 协议,比较稳定可靠,但占用资源多
// stream 为 false 时,表示为datagram socket 数据报套接字,使用 UDP 协议,不稳定,但占用资源少
private Socket(SocketAddress address, SocketAddress localAddr,boolean stream) throws IOException {setImpl();// backward compatibilityif (address == null)throw new NullPointerException();try {// 创建 socketcreateImpl(stream);// 如果 ip 地址不为空,绑定地址if (localAddr != null)// create、bind、connect 也是 native 方法bind(localAddr);connect(address);} catch (IOException | IllegalArgumentException | SecurityException e) {try {close();} catch (IOException ce) {e.addSuppressed(ce);}throw e;}
}

从源码中可以看出:

  1. 在构造 Socket 的时候,你可以选择 TCP 或 UDP,默认是 TCP;
  2. 如果构造 Socket 时,传入地址和端口,那么在构造的时候,就会尝试在此地址和端口上创建套接字;
  3. Socket 的无参构造器只会初始化 SocksSocketImpl,并不会和当前地址端口绑定,需要我们手动的调用 connect 方法,才能使用当前地址和端口;
  4. Socket 我们可以理解成网络沟通的语言层次的抽象,底层网络创建、连接和关闭,仍然是 TCP 或 UDP 本身网络协议指定的标准,Socket 只是使用 Java 语言做了一层封装,从而让我们更方便地使用。

3 connect 连接服务端

connect 方法主要用于 Socket 客户端连接上服务端,如果底层是 TCP 层协议的话,就是通过三次握手和服务端建立连接,为客户端和服务端之间的通信做好准备,底层源码如下:

public void connect(SocketAddress endpoint, int timeout) throws IOException {
}

connect 方法要求有两个入参,第一个入参是 SocketAddress,表示服务端的地址,我们可以使用 InetSocketAddress 进行初始化,比如:new InetSocketAddress(“www.wenhe.com”, 2000)。

第二入参是超时时间的意思(单位毫秒),表示客户端连接服务端的最大等待时间,如果超过当前等待时间,仍然没有成功建立连接,抛 SocketTimeoutException 异常,如果是 0 的话,表示无限等待。

4 Socket 常用设置参数

Socket 的常用设置参数在 SocketOptions 类中都可以找到,接下来我们来一一分析下,以下理解大多来自类注释和网络。

4.1 setTcpNoDelay

此方法是用来设置 TCP_NODELAY 属性的,属性的注释是这样的:此设置仅仅对 TCP 生效,主要为了禁止使用 Nagle 算法,true 表示禁止使用,false 表示使用,默认是 false。

对于 Nagle 算法,我们引用维基百科上的解释:

纳格算法是以减少数据包发送量来增进 [TCP/IP] 网络的性能,它由约翰·纳格任职于Ford Aerospace时命名。

纳格的文件[注 1]描述了他所谓的“小数据包问题”-某个应用程序不断地提交小单位的数据,且某些常只占1字节大小。因为TCP数据包具有40字节的标头信息(TCP与IPv4各占20字节),这导致了41字节大小的数据包只有1字节的可用信息,造成庞大的浪费。这种状况常常发生于Telnet工作阶段-大部分的键盘操作会产生1字节的数据并马上提交。更糟的是,在慢速的网络连线下,这类的数据包会大量地在同一时点传输,造成壅塞碰撞。

纳格算法的工作方式是合并(coalescing)一定数量的输出数据后一次提交。特别的是,只要有已提交的数据包尚未确认,发送者会持续缓冲数据包,直到累积一定数量的数据才提交。

总结算法开启关闭的场景:

  1. 如果 Nagle 算法关闭,对于小数据包,比如一次鼠标移动,点击,客户端都会立马和服务端交互,实时响应度非常高,但频繁的通信却很占用不少网络资源;
  2. 如果 Nagle 算法开启,算法会自动合并小数据包,等到达到一定大小(MSS)后,才会和服务端交互,优点是减少了通信次数,缺点是实时响应度会低一些。

Socket 创建时,默认是开启 Nagle 算法的,可以根据实时性要求来选择是否关闭 Nagle 算法。

4.2 setSoLinger

setSoLinger 方法主要用来设置 SO_LINGER 属性值的。

注释上大概是这个意思:在我们调用 close 方法时,默认是直接返回的,但如果给 SO_LINGER 赋值,就会阻塞 close 方法,在 SO_LINGER 时间内,等待通信双方发送数据,如果时间过了,还未结束,将发送 TCP RST 强制关闭 TCP 。

我们看一下 setSoLinger 源码:

// on 为 false,表示不启用延时关闭,true 的话表示启用延时关闭
// linger 为延时的时间,单位秒
public void setSoLinger(boolean on, int linger) throws SocketException {// 检查是否已经关闭if (isClosed())throw new SocketException("Socket is closed");// 不启用延时关闭if (!on) {getImpl().setOption(SocketOptions.SO_LINGER, new Boolean(on));// 启用延时关闭,如果 linger 为 0,那么会立即关闭// linger 最大为 65535 秒,约 18 小时} else {if (linger < 0) {throw new IllegalArgumentException("invalid value for SO_LINGER");}if (linger > 65535)linger = 65535;getImpl().setOption(SocketOptions.SO_LINGER, new Integer(linger));}
}

4.3 setOOBInline

setOOBInline 方法主要使用设置 SO_OOBINLINE 属性。

注释上说:如果希望接受 TCP urgent data(TCP 紧急数据)的话,可以开启该选项,默认该选项是关闭的,我们可以通过 Socket#sendUrgentData 方法来发送紧急数据。

查询了很多资料,都建议尽可能的去避免设置该值,禁止使用 TCP 紧急数据。

4.4 setSoTimeout

setSoTimeout 方法主要是用来设置 SO_TIMEOUT 属性的。

注释上说:用来设置阻塞操作的超时时间,阻塞操作主要有:

  1. ServerSocket.accept() 服务器等待客户端的连接;
  2. SocketInputStream.read() 客户端或服务端读取输入超时;
  3. DatagramSocket.receive()。

我们必须在必须在阻塞操作之前设置该选项, 如果时间到了,操作仍然在阻塞,会抛出 InterruptedIOException 异常(Socket 会抛出 SocketTimeoutException 异常,不同的套接字抛出的异常可能不同)。

对于 Socket 来说,超时时间如果设置成 0,表示没有超时时间,阻塞时会无限等待。

4.5 setSendBufferSize

setSendBufferSize 方法主要用于设置 SO_SNDBUF 属性的,入参是 int 类型,表示设置发送端(输出端)的缓冲区的大小,单位是字节。

入参 size 必须大于 0,否则会抛出 IllegalArgumentException 异常。

一般我们都是采取默认的,如果值设置太小,很有可能导致网络交互过于频繁,如果值设置太大,那么交互变少,实时性就会变低。

4.6 setReceiveBufferSize

setReceiveBufferSize 方法主要用来设置 SO_RCVBUF 属性的,入参是 int 类型,表示设置接收端的缓冲区的大小,单位是字节。

入参 size 必须大于 0,否则会抛出 IllegalArgumentException 异常。

一般来说,在套接字建立连接之后,我们可以随意修改窗口大小,但是当窗口大小大于 64k 时,需要注意:

  1. 必须在 Socket 连接客户端之前设置缓冲值;
  2. 必须在 ServerSocket 绑定本地地址之前设置缓冲值。

4.7 setKeepAlive

setKeepAlive 方法主要用来设置 SO_KEEPALIVE 属性,主要是用来探测服务端的套接字是否还是存活状态,默认设置是 false,不会触发这个功能。

如果 SO_KEEPALIVE 开启的话,TCP 自动触发功能:如果两小时内,客户端和服务端的套接字之间没有任何通信,TCP 会自动发送 keepalive 探测给对方,对方必须响应这个探测(假设是客户端发送给服务端),预测有三种情况:

  1. 服务端使用预期的 ACK 回复,说明一切正常;
  2. 服务端回复 RST,表示服务端处于死机或者重启状态,终止连接;
  3. 没有得到服务端的响应(会尝试多次),表示套接字已经关闭了。

4.8 setReuseAddress

setReuseAddress 方法主要用来设置 SO_REUSEADDR 属性,入参是布尔值,默认是 false。

套接字在关闭之后,会等待一段时间之后才会真正的关闭,如果此时有新的套接字前来绑定同样的地址和端口时,如果 setReuseAddress 为 true 的话,就可以绑定成功,否则绑定失败。

5 总结

如果平时一直在做业务代码,Socket 可能用到的很少,但面试问到网络协议时,或者以后有机会做做中间件的时候,就会有大概率会接触到 Socket,所以多学学,作为知识储备也蛮好的。

面试官系统精讲Java源码及大厂真题 - 45 Socket 源码及面试题相关推荐

  1. 面试官系统精讲Java源码及大厂真题 - 34 只求问倒:连环相扣系列锁面试题

    34 只求问倒:连环相扣系列锁面试题 自信和希望是青年的特权. 引导语 面试中,问锁主要是两方面:锁的日常使用场景 + 锁原理,锁的日常使用场景主要考察对锁 API 的使用熟练度,看看你是否真的使用过 ...

  2. 面试官系统精讲Java源码及大厂真题 - 48 一起看过的 Java 源码和面试真题

    48 一起看过的 Java 源码和面试真题 不为了源码而读源码,只为了更好的实践 持续几个月,我们的专栏终于结束了,这篇总结篇,我们又想回到当初写这篇专栏的初心:我们不为读源码而读源码,只是为了更好的 ...

  3. 面试官系统精讲Java源码及大厂真题 - 36 从容不迫:重写锁的设计结构和细节

    36 从容不迫:重写锁的设计结构和细节 受苦的人,没有悲观的权利. --尼采 引导语 有的面试官喜欢让同学在说完锁的原理之后,让你重写一个新的锁,要求现场在白板上写出大概的思路和代码逻辑,这种面试题目 ...

  4. 面试官系统精讲Java源码及大厂真题 - 38 线程池源码面试题

    38 线程池源码面试题 与有肝胆人共事,从无字句处读书. --周恩来 引导语 线程池在日常面试中占比很大,主要是因为线程池内容涉及的知识点较广,比如涉及到队列.线程.锁等等,所以很多面试官喜欢把线程池 ...

  5. 面试官系统精讲Java源码及大厂真题 - 24 举一反三:队列在 Java 其它源码中的应用

    24 举一反三:队列在 Java 其它源码中的应用 世上无难事,只要肯登攀. 引导语 队列除了提供 API 供开发者使用外,自身也和 Java 中其他 API 紧密结合,比如线程池和锁,线程池直接使用 ...

  6. 面试官系统精讲Java源码及大厂真题 - 17 并发 List、Map源码面试题

    17 并发 List.Map源码面试题 梦想只要能持久,就能成为现实.我们不就是生活在梦想中的吗? 引导语 并发 List 和 Map 是技术面时常问的问题,问的问题也都比较深入,有很多问题都是面试官 ...

  7. 面试官系统精讲Java源码及大厂真题 - 11 HashSet、TreeSet 源码解析

    11 HashSet.TreeSet 源码解析 更新时间:2019-09-16 19:37:35 成功的奥秘在于目标的坚定. --迪斯雷利 引导语 HashSet.TreeSet 两个类是在 Map ...

  8. 面试官系统精讲Java源码及大厂真题 - 10 Map源码会问哪些面试题

    10 Map源码会问哪些面试题 更新时间:2019-09-10 10:34:08 人的一生可能燃烧也可能腐朽,我不能腐朽,我愿意燃烧起来! --奥斯特洛夫斯基 引导语 Map 在面试中,占据了很大一部 ...

  9. 面试官系统精讲Java源码及大厂真题 - 07 List 源码会问哪些面试题

    07 List 源码会问哪些面试题 勤学如春起之苗,不见其增,日有所长. --陶潜 引导语 List 作为工作中最常见的集合类型,在面试过程中,也是经常会被问到各种各样的面试题,一般来说,只要你看过源 ...

最新文章

  1. 专访1药网技术副总裁黄哲铿:揭秘技术跨界管理之道
  2. Android 屏幕自动旋转-Sensor属性
  3. hihocoder 第113周 Fibonacci(动态规划)
  4. 如何发表顶级期刊论文
  5. DataGridView常见用法和FAQ汇总
  6. 牛人的博客(机器学习,图像处理,计算机视觉)
  7. 1650显卡学计算机,适合老电脑升级?GTX1650显卡开箱,性价比依然不高!
  8. rest api_摆脱困境:向REST API添加验证
  9. 图解 Java 常用数据结构
  10. python选择排序 时间复杂度
  11. mysql8 win10_window10下安装多个MySQL8.0
  12. [转]硬盘分区表知识——详解硬盘MBR
  13. Dirichlet energy and the Laplace equation
  14. 【问题解决】线程间操作无效:从不是创建控件“textBox1”的线程访问它
  15. IntelliJ Idea一些常用快捷键
  16. 系统学习机器学习之误差理论
  17. 【读书笔记——开关电源】《精通开关电源设计》(1)
  18. PCB和PCBA设计及标准
  19. uln2003驱动蜂鸣器_51单片机蜂鸣器
  20. 【Vue.$delete】 删除

热门文章

  1. Net 4.0并行库实用性演练
  2. 网站优化:浏览器缓存控制简介及配置策略
  3. Swift快速参考手册
  4. Magento 创建优惠卷 Create unique coupon code in Magento
  5. cakephp 1.3 Controller
  6. lua——赢三张牌型处理相关算法(上)——牌值数据
  7. Linux——如何打开或者安装g++
  8. Java—Object类及自定义克隆详解
  9. Ubuntu 14.04 或者16.04开启root账户登录和图形界面登录root时候的报错解决方法
  10. 测试 第七章 循环结构进阶 1205 草稿