基础
所谓 socket 通常也称作”套接字”,用于描述 IP 地址和端口,是一个通信链的句柄。网络上具有唯一标识的 IP 地址和端口组合在一起才能构成唯一能识别的标识符套接字,应用程序通常通过”套接字”向网络发出请求或者应答网络请求。
几个常见类
InetAddress
用于标识网络上的硬件资源,主要表示IP地址。SocketAddress
表示Sokcet地址,不包含使用的具体协议,是一个抽象类。InetSocketAddress
继承了SocketAddress,包含主机名,IP地址和端口(hostname, InetAddress, port)。Socket
客户端Socket,默认采用的传输层协议为TCP。ServerSocket
服务器Socket,默认采用的传输层协议为TCP。等待客户端请求,并基于这些请求执行操作返回一个结果。SocketImpl
通用抽象类,用来创建客户端和服务端具体的Socket。Socket通信流程的所有方法都是通过该类和子类实现的。DatagramPacket
表示存放数据的数据报。DatagramSocket
实现了一个发送和接收数据报的socket,传输层协议使用UDP。客户端和服务端都使用该套接字来实现通信。
传输协议
TCP:Tranfer Control Protocol
是一种面向连接的保证可靠传输的协议,通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个server socket等待建立连接accept时,客户端socket可以要求进行连接。一旦连接起来后,它们就可以进行双向数据传输,双方都可以进行发送或接收数据。UDP:User Datagram Protocol
是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。传输数据时数据报大小必须限定在64KB之内。
端口
区分一台主机的多个不同应用程序,端口号范围为 0-65535,其中 0-1023 位为系统保留。如:HTTP:80 FTP:21 Telnet:23。
五元组
通信术语,通常是指:源 IP 地址,源端口,目的 IP 地址,目的端口和传输层协议,两台计算机通信必须要明确指定五元组的值。
- 地址和端口
Socket中包含了源和目标的IP地址,端口。 - 传输协议类型
Socket表示TCP协议;DatagramSocket表示UDP协议。
输入输出流读写
Soket 数据连接是全双工的,一旦建立连接可以得到 Socket 的输入流和输出流,主机可以使用这两个流同时发送和接受数据。流是同步的,也就是说当请求流读/写一段数据时,阻塞等待直到有数据。Java 还支持使用通道和缓冲区的非阻塞 I/O(NIO),暂不讨论。
socket.getInputStream()
获取输入流,通过输入流读取数据。socket.getOutputStream()
获取输出流,通过输出流写入数据。
??关闭一个流也会也会关闭一个 Sokcet 连接。??对于同一个 Socket,如果关闭了输出流,则与该输出流关联的 Socket 也会被关闭,所以一般不用关闭流,直接关闭 Socket 即可。
Socket 原理机制
- 通信的两端都有
Socket - 网络通信其实就是
Socket间的通信 - 数据在两个
Socket间通过IO传输
四种常见异常
以下四种类型异常都是继承于 IOException,所以很多之后直接弹出 IOException 即可。
UnkownHostException:主机名字或IP错误ConnectException:服务器拒绝连接、服务器没有启动、超出队列数,拒绝连接等SocketTimeoutException:连接超时BindException:Socket对象无法与指定的本地IP地址或端口绑定
Socket
构造方法
1 | Socket(String host, int port)throws UnknownHostException, IOException |
Socket 有多个构造方法,但是最终调用的是最后一个。至少需要指定目的主机名 host 或者 IP 地址,以及端口号 port。
参数解析:
address
远程即服务端,包含目标IP地址,主机名,端口。不能为空,否则会抛出异常。localAddr
本地即客户端,包含源IP地址,主机名,端口。如果为空,表示系统自动分配。stream
为true表示使用流式连接即TCP方式;为false表示使用数据报即UDP方式。默认为true。
关闭
在 try - finally 块中采用 close-if-not-null 来关闭 Socket 连接。
Socket 选项
TCP_NODELAY
设置为true可以保证无论包的大小都会尽快发送,不用缓冲到足够大的数据包。SO_LINGER
指定Socket关闭时,还没有发送的数据包如何处理。默认情况下close方法会立即返回,但系统会尝试发送剩余的数据。如果延迟时间设置为 0,所有未发送数据都会被丢弃。如果设置了事件,close会阻塞到指定时间,等待数据接收和确认,如果超出时间,剩余数据将会被丢弃。SO_TIMEOUTSocket在读取数据时,read()会阻塞尽可能长的时间。如果设置后,阻塞时间不会超过指定时间,否则会抛出异常,但是Socket仍然是处于连接状态,下次可以继续读。SO_RCVBUFTCP使用缓冲区提升性能,参数为设置接受缓冲区大小。SO_SNDBUF
设置发送缓冲区大小。SO_KEEPALIVE
默认值为false,如果打开,在Sokcet没有数据传输时,每两个小时客户端会发送一个数据包确保服务器是正常的。SO_OOBINLINE
参数被设置时,TCP会发送一个紧急数据包,接受方收到后会优先处理。SO_REUSEADDR
设置Socket是否可以重复使用端口,默认是也可以的。IP_TOS
设置数据拥堵时的处理策略。
常见 API
socket.getRemoteSocketAddress()
获取目的IP地址,主机名,端口。socket.getInetAddress()
获取目的IP地址,主机名。socket.getPort()
获取目的端口。socket.getLocalSocketAddress()
获取源IP地址,主机名,端口。socket.getLocalAddress()
获取源IP地址,主机名。socket.getLocalPort()
获取源端口。
SocketServer
构造方法
1 | public ServerSocket() throws IOException |
参数解析:
- port
服务器端口号,0 表示自动分配的端口。 - backlog
请求链接队列的最大长度。 - bindAddr
服务器将绑定的本地地址InetAddress
关闭
在 try - finally 块中采用 close-if-not-null 来关闭 SocketServer 连接。
Socket 选项
SO_TIMEOUT
设置Socket阻塞的时间,如:accept, read, receive。SO_REUSEADDR
设置Socket是否可以重复使用端口,默认是也可以的。SO_RCVBUF
设置Socket接受数据缓冲区大小。
日志 log
服务器需要形成记录日志的习惯,来记录客户端访问信息,出现的错误等等。
多线程设计
通常服务端会有多个客户端访问,所以可以设置一个线程池来处理每个客户端的请求,避免阻塞。
常见 API
serverSocket.getLocalSocketAddress()
获取本地IP地址,主机名,端口。典型值为0.0.0.0/0.0.0.0:***。serverSocket.getInetAddress()
获取本地IP地址,典型值为0.0.0.0/0.0.0.0,即自动分配的本地地址。serverSocket.getLocalPort
获取本地端口,即Socket通信服务端中的目的端口。
客户端和服务端的 TCP 通信步骤
通信流程图

大概流程如下:
- 服务端
SocketServer初始化 - 服务端等待并接受客户端的连接
accept - 客户端
Socket初始化 - 数据通信
- 关闭输入输出流,再关闭
Socket/SocketServer

服务端 SocketServer 初始化
- 创建
SocketSocketImpl.java: protected abstract void create(boolean stream) throws IOException;,创建服务端Socket。 - 绑定
bindSocketImpl.java: protected abstract void bind(InetAddress host, int port) throws IOException;,显示指定或者系统自动分配本地IP地址及空闲端口,绑定后形成目的IP地址和端口。 - 监听
listenSocketImpl.java: protected abstract void listen(int backlog) throws IOException;,设置请求连接队列的最大值。
这三步都在 ServerSocket 的构造方法中实现,不用显示调用。
服务端等待接受连接 accept
1 | SocketServer.java: public Socket accept() throws IOException; |
阻塞等待,监听和接受客户端的 Socket 连接,并返回服务端的 Socket,最终会调用 SocketImpl 的方法接受连接。
客户端 Socket 初始化
- 创建
SocketSocketImpl.java: protected abstract void create(boolean stream) throws IOException;,创建客户端Socket。 - 绑定
bindSocketImpl.java: protected abstract void bind(InetAddress host, int port) throws IOException;,显示指定或者系统自动分配本地IP地址及空闲端口,绑定后形成源IP地址和端口。 - 连接
connect1
2
3
4
5Socket.java: public void connect(SocketAddress endpoint) throws IOException
Socket.java: public void connect(SocketAddress endpoint, int timeout) throws IOException
SocketImpl.java: protected abstract void connect(String host, int port) throws IOException;
SocketImpl.java: protected abstract void connect(InetAddress address, int port) throws IOException;
SocketImpl.java: protected abstract void connect(SocketAddress address, int timeout) throws IOException;
SocketAddress 包含目标 IP 地址和端口,在指定时间内连接服务端的 Socket。如果 timeout 为 0,表示不限时。
这三步都在 Socket 的构造方法中实现,不用显示调用。也就是说客户端在 TCP 协议的 Socket 编程中,非常简单,只需要指定目的 SocketAddress,就可以直接获取输入输出流来读写数据。
数据通信
- 服务端建立连接后
通过输入流读取客户端发送的请求信息;通过输出流向客户端发送响应信息。 - 客户端建立连接后
通过输出流向服务器端发送请求信息;通过输入流读取服务端响应的信息。
关闭输入输出流,再关闭 Socket
ServerSocket 和 Socket 没有先后关闭的必然顺序。ServerSocket 先关闭,并不会影响当前已经连接的 Sokcet
TCP 通信示例
流程图

服务端
1 | public class TestTCPSocketServer { |
大致思路:
- 初始化线程池,用于接受并执行客户端的请求任务
- 创建
ServerSocket对象,绑定监听端口 - 开启后台线程,监听终端输入
exit/quit - 循环调用
accept()方法,监听客户端请求 - 接收到请求后,为每个客户端创建
Socket专线连接 - 线程池为每个客户端提供一个单独线程用于
Socket通信 - 服务器端继续等待新的客户端连接
- 独立线程中,通过输入流读取客户端发送的请求信息,通过输出流向客户端发送响应信息
- 客户端断开连接后,关闭相关资源,并关闭该
Socket,释放当前线程 - 服务端接收到终端输入退出指令后,关闭线程池并关闭
ServerSocket
客户端
1 | public class TestTCPSocketClient { |
大致思路:
- 创建
Socket对象,指明需要连接的服务器的地址和端口号 - 连接建立后,从终端读取输入信息
- 通过输出流向服务器端发送读取到的信息
- 通过输入流获取服务器响应的信息
- 关闭相关资源,关闭
Socket
DatagramPacket
数据报(Datagram):将数据填充到 UDP 包中。数据报存放到 DatagramPacket 中,而 DatagramSocket 用来收发数据报。数据报的所有信息都包含在这个包中(包括发往的目标地址),Socket 只需要了解端口监听和发送。UDP 这种模式并没有像 TCP 那样两台主机有唯一连接的概念,一个 Socket 会收发所有指定端口的数据,而不会关心对方是哪个主机。 DatagramSocket 可以从多个独立主机收发数据,与 TCP 不同,这个 Socket 并不会专用于某个连接。TCP 中 Socket 把网络连接看作是一个流,通过输入输出流来收发数据。但是 UDP 处理的总是单个数据报包,填充在数据报包中的所有数据会以一个包的形式发送,这些数据要么全部接受,要么全部丢弃。一个包与另一个包并不一定相关,而且无法确认先后顺序。
对于流必须提供数据的有序队列,而数据报会尽可能快的发送到接收方。
UDP 服务端通常不需要使用多线程,不会阻塞等待客户端的响应,除非为了应对大量耗时工作才会使用多线程。
UDP 数据报是基于 IP 数据报建立的,只向其底层 IP 数据添加了很少的内容(8 个字节的首部信息)。UDP 包中数据的理论长度为 65507 个字节,但实际上很多平台限制往往是 8KB (8192 字节),大多数情况下更大的包会被简单的截取为 8KB。并且为了保证安全性,UDP 包的数据部分应该尽量少于 512 字节。
构造方法
1 | // 接受数据报 |
注意:
DatagramPacket虽然都是构造方法,但是有 2 个是接收数据报的构造方法,剩余 4 个是发送数据报的构造方法。普通情况下构造方法主要用于不同对象提供不同的类型信息,而不是像数据报这样提供不同的功能对象。
参数解析:
buf[]
保存接收或发送数据的数组,也就是不管是发送还是接受拿到的都是字节数组。length
数组中用于接受或发送数据的长度。offset
数组的偏移量,默认为 0。InetAddress
发送数据时接收方的IP地址。port
发送数据时接收方的端口。SocketAddress
发送数据时接收方的地址和端口
get 方法
get 方法可以获取构造方法中传进来的所有参数信息。
1 | // 获取 IP 地址和端口 |
set 方法
set 方法可以在构造方法创建数据报后,改变所有的数据报信息,相当于重新创建了一个数据报。因为 DatagramPacket 对象的重复创建和垃圾回收影响性能,所以重用对象比构造对象快的多。
1 | // 设置 IP 地址和端口 |
注意:所有的
set方法都加了同步锁。
DatagramSocket
要收发数据报,需要先打开一个 DatagramSocket,而所有的 DatagramSocket 必须绑定一个本地端口,这个端口用来监听收发的数据,并写入数据报的首部。DatagramSocket 只存储本地地址和端口,所有的远程地址和端口都在 DatagramPacket 中,所以 DatagramSocket 可以同时和多台主机收发数据(只要 DatagramPacket 不同就可以了)。
构造方法
1 | public class DatagramSocket implements java.io.Closeable { |
收发数据报
1 | public void send(DatagramPacket p) throws IOException {...} |
send 用来发送数据报;receive 加了同步锁,并且会阻塞当前线程,直到有数据到达,接收一个数据报。
常见 API
1 | // 关闭 socket |
Socket 选项
SO_TIMEOUTSocket在接收数据时,receive()会阻塞尽可能长的时间。如果设置后,阻塞时间不会超过指定时间,否则会抛出异常。设置为 0 表示永不超时。SO_RCVBUFUDP设置网络接收缓冲区大小,与TCP对比,UDP应该设置足够大的缓冲区。SO_SNDBUF
建议发送缓冲区大小,但是操作系统可以忽略这个建议。SO_REUSEADDR
和TCP Socket的意义不同,设置后表示是否允许多个数据报同时绑定到相同的端口和地址。重用端口通常会用于UDP组播。SO_BROADCAST
控制是否允许一个Socket向广播地址收发包,默认是打开的。UDP广播通常用于DHCP协议,路由器和网关一般不转发广播消息,但仍然会在本地网络中带来大量业务流。IP_TOS
设置数据拥堵时的处理策略。
UDP 通信示例
流程图

服务端
1 | public class TestDayTimeUDPServer { |
大致思路:
- 创建
DatagramSocket,绑定端口号 - 创建
DatagramPacket,指定目标地址和端口 - 阻塞等待接收客户端发送的数据
- 向客户端发送响应数据
客户端
1 | public class TestUDPGetDayTime { |
大致思路:
- 定义发送信息,并存储到数组中
- 创建
DatagramPacket,指定目标地址和端口,数据数组 - 创建
DatagramSocket,绑定本地端口 - 向服务端请求数据
- 从服务端获取响应数据