## 45 Socket 源码及面试题
## 引导语
Socket 中文翻译叫套接字,可能很多工作四五年的同学都没有用过这个 API,但只要用到这个 API 时,必然是在重要的工程的核心代码处。
大家平时基本都在用开源的各种 rpc 框架,比如说 Dubbo、gRPC、Spring Cloud 等等,很少需要手写网络调用,以下三小节可以帮助大家补充这块的内容,当你真正需要的时候,可以作为手册示例。
本文和《ServerSocket 源码及面试题》一文主要说 Socket 和 ServerSocket 的源码,《工作实战:Socket 结合线程池的使用》这章主要说两个 API 在实际工作中如何落地。
### 1 Socket 整体结构
Socket 的结构非常简单,Socket 就像一个壳一样,将套接字初始化、创建连接等各种操作包装了一下,其底层实现都是 SocketImpl 实现的,Socket 本身的业务逻辑非常简单。
Socket 的属性不多,有套接字的状态,SocketImpl,读写的状态等等,源码如下图:
![](https://img.kancloud.cn/fe/e1/fee1366a61edd2e9106ffa1c2003f044_496x264.jpg)
套接字的状态变更都是有对应操作方法的,比如套接字新建(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 compatibility if (address == null) throw new NullPointerException();
try { // 创建 socket createImpl(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 方法时,默认是直接返回的,但如果给 SOLINGER 赋值,就会阻塞 close 方法,在 SOLINGER 时间内,等待通信双方发送数据,如果时间过了,还未结束,将发送 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 的话,就可以绑定成功,否则绑定失败。
- 前言
- 第1章 基础
- 01 开篇词:为什么学习本专栏
- 02 String、Long 源码解析和面试题
- 03 Java 常用关键字理解
- 04 Arrays、Collections、Objects 常用方法源码解析
- 第2章 集合
- 05 ArrayList 源码解析和设计思路
- 06 LinkedList 源码解析
- 07 List 源码会问哪些面试题
- 08 HashMap 源码解析
- 09 TreeMap 和 LinkedHashMap 核心源码解析
- 10 Map源码会问哪些面试题
- 11 HashSet、TreeSet 源码解析
- 12 彰显细节:看集合源码对我们实际工作的帮助和应用
- 13 差异对比:集合在 Java 7 和 8 有何不同和改进
- 14 简化工作:Guava Lists Maps 实际工作运用和源码
- 第3章 并发集合类
- 15 CopyOnWriteArrayList 源码解析和设计思路
- 16 ConcurrentHashMap 源码解析和设计思路
- 17 并发 List、Map源码面试题
- 18 场景集合:并发 List、Map的应用场景
- 第4章 队列
- 19 LinkedBlockingQueue 源码解析
- 20 SynchronousQueue 源码解析
- 21 DelayQueue 源码解析
- 22 ArrayBlockingQueue 源码解析
- 23 队列在源码方面的面试题
- 24 举一反三:队列在 Java 其它源码中的应用
- 25 整体设计:队列设计思想、工作中使用场景
- 26 惊叹面试官:由浅入深手写队列
- 第5章 线程
- 27 Thread 源码解析
- 28 Future、ExecutorService 源码解析
- 29 押宝线程源码面试题
- 第6章 锁
- 30 AbstractQueuedSynchronizer 源码解析(上)
- 31 AbstractQueuedSynchronizer 源码解析(下)
- 32 ReentrantLock 源码解析
- 33 CountDownLatch、Atomic 等其它源码解析
- 34 只求问倒:连环相扣系列锁面试题
- 35 经验总结:各种锁在工作中使用场景和细节
- 36 从容不迫:重写锁的设计结构和细节
- 第7章 线程池
- 37 ThreadPoolExecutor 源码解析
- 38 线程池源码面试题
- 39 经验总结:不同场景,如何使用线程池
- 40 打动面试官:线程池流程编排中的运用实战
- 第8章 Lambda 流
- 41 突破难点:如何看 Lambda 源码
- 42 常用的 Lambda 表达式使用场景解析和应用
- 第9章 其他
- 43 ThreadLocal 源码解析
- 44 场景实战:ThreadLocal 在上下文传值场景下的实践
- 45 Socket 源码及面试题
- 46 ServerSocket 源码及面试题
- 47 工作实战:Socket 结合线程池的使用
- 第10章 专栏总结
- 48 一起看过的 Java 源码和面试真题