不啰嗦,我们直接开始!

引导语

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 源码 - Socket 源码及面试题相关推荐

  1. 一对一直播社交聊天程序: JAVA中的Socket源码概述

    1.网络编程简要概述: 网络编程实质实质就是两个(或多个)设备(例如计算机)之间的数据传输.而要实现两台计算机通过互联网连接进行数据传输,必输要满足计算机网络的5层协议(物理层,数据链路层,网络层,运 ...

  2. 面试官系统精讲Java源码及大厂真题 - 45 Socket 源码及面试题

    45 Socket 源码及面试题 引导语 Socket 中文翻译叫套接字,可能很多工作四五年的同学都没有用过这个 API,但只要用到这个 API 时,必然是在重要的工程的核心代码处. 大家平时基本都在 ...

  3. 【java毕业设计】基于java+原生Sevlet+socket的聊天室系统设计与实现(毕业论文+程序源码)——聊天室系统

    基于java+原生Sevlet+socket的聊天室系统设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于java+原生Sevlet+socket的聊天室系统设计与实现,文章末尾附有本毕业设 ...

  4. java的String类源码详解

    java的String类源码详解 类的定义 public final class Stringimplements java.io.Serializable, Comparable<String ...

  5. 死磕 java集合之ArrayDeque源码分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...

  6. java 头尾 队列_源码|jdk源码之栈、队列及ArrayDeque分析

    栈.队列.双端队列都是非常经典的数据结构.和链表.数组不同,这三种数据结构的抽象层次更高.它只描述了数据结构有哪些行为,而并不关心数据结构内部用何种思路.方式去组织. 本篇博文重点关注这三种数据结构在 ...

  7. java function获取参数_「Java容器」ArrayList源码,大厂面试必问

    ArrayList简介 ArrayList核心源码 ArrayList源码分析 System.arraycopy()和Arrays.copyOf()方法 两者联系与区别 ArrayList核心扩容技术 ...

  8. Java Review - LinkedHashMap LinkedHashSet 源码解读

    文章目录 Pre 概述 数据结构 类继承关系 构造函数 方法 get() put() remove() LinkedHashSet 使用案例 - FIFO策略缓存 Pre Java Review - ...

  9. 手把手教你,Java如何实现二维码?【附源码】

    作者:红颜祸水nvn 来源:http://suo.im/5R6ewH 步骤1 第一步首先创建一个普通的 Maven 项目,然后要实现二维码功能,我们肯定要使用别人提供好的 Jar 包,这里我用的是 g ...

最新文章

  1. JavaScript onerror事件
  2. Spring @RequestMapping注解示例
  3. docker java 中文乱码_java使用awt包在生产环境docker部署时出现中文乱码的处理
  4. vue中使用this遇到的坑
  5. 如何将特定提交推送到远程,而不是之前的提交?
  6. Deepin 添加PPA源问题
  7. VS2013打开项目出现未找到与约束contractname 匹配的导出的错误
  8. Rust: codewars的Highest and Lowest
  9. SQL盲注中的部分常用函数
  10. 【MySQL】增大字符串长度不会锁表吗
  11. 【Python字符串】
  12. 童鞋想盗取我十几个G的“种子”,看我是用python来层层加锁!!!
  13. ROS kinetic 机器视觉
  14. 研究生要不要出去实习
  15. html5前端工程师简历,前端工程师简历自我评价填写样本
  16. 2019年校招实习免费内推(含面试资料简历模板)
  17. LInux背景【Linux】
  18. linux shell对行数进行统计的方法
  19. 深度linux运行速度慢,Deepin(深度操作系统)解决老电脑卡顿,慢等问题
  20. WordPress多语言版本切换插件Translate WordPress with GTranslate

热门文章

  1. Word、PPT、Visio里面怎么插入latex mathcal的数学字符?
  2. 关于loop unwinding
  3. 低轨卫星传播特性仿真与分析
  4. 图片格式导致的报错:Attribute Error: ‘NoneType‘ object has no attribute ‘astype‘ 解决方案
  5. 【ESD专题】ESD和EOS有什么差异?
  6. phonex的使用,二级索引,预分区,调优
  7. 【学习笔记】Python基础入门知识笔记,万字攻略带你走进Python编程
  8. lect01_codes_高阶语法
  9. 如何下载网站中的图片元素
  10. 【深度学习】深度学习基础-Warm_up训练策略