Java 源码 - 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 的构造器比较多,可以分成两大类:
- 指定代理类型(Proxy)创建套节点,一共有三种类型为:DIRECT(直连)、HTTP(HTTP、FTP 高级协议的代理)、SOCKS(SOCKS 代理),三种不同的代码方式对应的 SocketImpl 不同,分别是:PlainSocketImpl、HttpConnectSocketImpl、SocksSocketImpl,除了类型之外 Proxy 还指定了地址和端口;
- 默认 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;}
}
从源码中可以看出:
- 在构造 Socket 的时候,你可以选择 TCP 或 UDP,默认是 TCP;
- 如果构造 Socket 时,传入地址和端口,那么在构造的时候,就会尝试在此地址和端口上创建套接字;
- Socket 的无参构造器只会初始化 SocksSocketImpl,并不会和当前地址端口绑定,需要我们手动的调用 connect 方法,才能使用当前地址和端口;
- 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)一定数量的输出数据后一次提交。特别的是,只要有已提交的数据包尚未确认,发送者会持续缓冲数据包,直到累积一定数量的数据才提交。
总结算法开启关闭的场景:
- 如果 Nagle 算法关闭,对于小数据包,比如一次鼠标移动,点击,客户端都会立马和服务端交互,实时响应度非常高,但频繁的通信却很占用不少网络资源;
- 如果 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 属性的。
注释上说:用来设置阻塞操作的超时时间,阻塞操作主要有:
- ServerSocket.accept() 服务器等待客户端的连接;
- SocketInputStream.read() 客户端或服务端读取输入超时;
- 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 时,需要注意:
- 必须在 Socket 连接客户端之前设置缓冲值;
- 必须在 ServerSocket 绑定本地地址之前设置缓冲值。
4.7、setKeepAlive
setKeepAlive 方法主要用来设置 SO_KEEPALIVE 属性,主要是用来探测服务端的套接字是否还是存活状态,默认设置是 false,不会触发这个功能。
如果 SO_KEEPALIVE 开启的话,TCP 自动触发功能:如果两小时内,客户端和服务端的套接字之间没有任何通信,TCP 会自动发送 keepalive 探测给对方,对方必须响应这个探测(假设是客户端发送给服务端),预测有三种情况:
- 服务端使用预期的 ACK 回复,说明一切正常;
- 服务端回复 RST,表示服务端处于死机或者重启状态,终止连接;
- 没有得到服务端的响应(会尝试多次),表示套接字已经关闭了。
4.8、setReuseAddress
setReuseAddress 方法主要用来设置 SO_REUSEADDR 属性,入参是布尔值,默认是 false。
套接字在关闭之后,会等待一段时间之后才会真正的关闭,如果此时有新的套接字前来绑定同样的地址和端口时,如果 setReuseAddress 为 true 的话,就可以绑定成功,否则绑定失败。
5、总结
如果平时一直在做业务代码,Socket 可能用到的很少,但面试问到网络协议时,或者以后有机会做做中间件的时候,就会有大概率会接触到 Socket,所以多学学,作为知识储备也蛮好的。
不啰嗦,文章结束,期待三连!
Java 源码 - Socket 源码及面试题相关推荐
- 一对一直播社交聊天程序: JAVA中的Socket源码概述
1.网络编程简要概述: 网络编程实质实质就是两个(或多个)设备(例如计算机)之间的数据传输.而要实现两台计算机通过互联网连接进行数据传输,必输要满足计算机网络的5层协议(物理层,数据链路层,网络层,运 ...
- 面试官系统精讲Java源码及大厂真题 - 45 Socket 源码及面试题
45 Socket 源码及面试题 引导语 Socket 中文翻译叫套接字,可能很多工作四五年的同学都没有用过这个 API,但只要用到这个 API 时,必然是在重要的工程的核心代码处. 大家平时基本都在 ...
- 【java毕业设计】基于java+原生Sevlet+socket的聊天室系统设计与实现(毕业论文+程序源码)——聊天室系统
基于java+原生Sevlet+socket的聊天室系统设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于java+原生Sevlet+socket的聊天室系统设计与实现,文章末尾附有本毕业设 ...
- java的String类源码详解
java的String类源码详解 类的定义 public final class Stringimplements java.io.Serializable, Comparable<String ...
- 死磕 java集合之ArrayDeque源码分析
问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...
- java 头尾 队列_源码|jdk源码之栈、队列及ArrayDeque分析
栈.队列.双端队列都是非常经典的数据结构.和链表.数组不同,这三种数据结构的抽象层次更高.它只描述了数据结构有哪些行为,而并不关心数据结构内部用何种思路.方式去组织. 本篇博文重点关注这三种数据结构在 ...
- java function获取参数_「Java容器」ArrayList源码,大厂面试必问
ArrayList简介 ArrayList核心源码 ArrayList源码分析 System.arraycopy()和Arrays.copyOf()方法 两者联系与区别 ArrayList核心扩容技术 ...
- Java Review - LinkedHashMap LinkedHashSet 源码解读
文章目录 Pre 概述 数据结构 类继承关系 构造函数 方法 get() put() remove() LinkedHashSet 使用案例 - FIFO策略缓存 Pre Java Review - ...
- 手把手教你,Java如何实现二维码?【附源码】
作者:红颜祸水nvn 来源:http://suo.im/5R6ewH 步骤1 第一步首先创建一个普通的 Maven 项目,然后要实现二维码功能,我们肯定要使用别人提供好的 Jar 包,这里我用的是 g ...
最新文章
- JavaScript onerror事件
- Spring @RequestMapping注解示例
- docker java 中文乱码_java使用awt包在生产环境docker部署时出现中文乱码的处理
- vue中使用this遇到的坑
- 如何将特定提交推送到远程,而不是之前的提交?
- Deepin 添加PPA源问题
- VS2013打开项目出现未找到与约束contractname 匹配的导出的错误
- Rust: codewars的Highest and Lowest
- SQL盲注中的部分常用函数
- 【MySQL】增大字符串长度不会锁表吗
- 【Python字符串】
- 童鞋想盗取我十几个G的“种子”,看我是用python来层层加锁!!!
- ROS kinetic 机器视觉
- 研究生要不要出去实习
- html5前端工程师简历,前端工程师简历自我评价填写样本
- 2019年校招实习免费内推(含面试资料简历模板)
- LInux背景【Linux】
- linux shell对行数进行统计的方法
- 深度linux运行速度慢,Deepin(深度操作系统)解决老电脑卡顿,慢等问题
- WordPress多语言版本切换插件Translate WordPress with GTranslate
热门文章
- Word、PPT、Visio里面怎么插入latex mathcal的数学字符?
- 关于loop unwinding
- 低轨卫星传播特性仿真与分析
- 图片格式导致的报错:Attribute Error: ‘NoneType‘ object has no attribute ‘astype‘ 解决方案
- 【ESD专题】ESD和EOS有什么差异?
- phonex的使用,二级索引,预分区,调优
- 【学习笔记】Python基础入门知识笔记,万字攻略带你走进Python编程
- lect01_codes_高阶语法
- 如何下载网站中的图片元素
- 【深度学习】深度学习基础-Warm_up训练策略