基础
所谓 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_TIMEOUT
Socket
在读取数据时,read()
会阻塞尽可能长的时间。如果设置后,阻塞时间不会超过指定时间,否则会抛出异常,但是Socket
仍然是处于连接状态,下次可以继续读。SO_RCVBUF
TCP
使用缓冲区提升性能,参数为设置接受缓冲区大小。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
初始化
- 创建
Socket
SocketImpl.java: protected abstract void create(boolean stream) throws IOException;
,创建服务端Socket
。 - 绑定
bind
SocketImpl.java: protected abstract void bind(InetAddress host, int port) throws IOException;
,显示指定或者系统自动分配本地IP
地址及空闲端口,绑定后形成目的IP
地址和端口。 - 监听
listen
SocketImpl.java: protected abstract void listen(int backlog) throws IOException;
,设置请求连接队列的最大值。
这三步都在 ServerSocket
的构造方法中实现,不用显示调用。
服务端等待接受连接 accept
1 | SocketServer.java: public Socket accept() throws IOException; |
阻塞等待,监听和接受客户端的 Socket
连接,并返回服务端的 Socket
,最终会调用 SocketImpl
的方法接受连接。
客户端 Socket
初始化
- 创建
Socket
SocketImpl.java: protected abstract void create(boolean stream) throws IOException;
,创建客户端Socket
。 - 绑定
bind
SocketImpl.java: protected abstract void bind(InetAddress host, int port) throws IOException;
,显示指定或者系统自动分配本地IP
地址及空闲端口,绑定后形成源IP
地址和端口。 - 连接
connect
1
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_TIMEOUT
Socket
在接收数据时,receive()
会阻塞尽可能长的时间。如果设置后,阻塞时间不会超过指定时间,否则会抛出异常。设置为 0 表示永不超时。SO_RCVBUF
UDP
设置网络接收缓冲区大小,与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
,绑定本地端口 - 向服务端请求数据
- 从服务端获取响应数据