Earth Guardian

You are not LATE!You are not EARLY!

0%

Java Socket 编程

基础

所谓 socket 通常也称作”套接字”,用于描述 IP 地址和端口,是一个通信链的句柄。网络上具有唯一标识的 IP 地址和端口组合在一起才能构成唯一能识别的标识符套接字,应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

几个常见类

  • InetAddress
    用于标识网络上的硬件资源,主要表示 IP 地址。
  • SocketAddress
    表示 Sokcet 地址,不包含使用的具体协议,是一个抽象类。
  • InetSocketAddress
    继承了 SocketAddress,包含主机名,IP 地址和端口(hostname, InetAddress, port)。
  • Socket
    客户端 Socket,默认采用的传输层协议为 TCP
  • ServerSocket
    服务器 Socket,默认采用的传输层协议为 TCP。等待客户端请求,并基于这些请求执行操作返回一个结果。
  • SocketImpl
    通用抽象类,用来创建客户端和服务端具体的 SocketSocket 通信流程的所有方法都是通过该类和子类实现的。
  • 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/ONIO),暂不讨论。

  • socket.getInputStream()
    获取输入流,通过输入流读取数据。
  • socket.getOutputStream()
    获取输出流,通过输出流写入数据。

??关闭一个流也会也会关闭一个 Sokcet 连接。??对于同一个 Socket,如果关闭了输出流,则与该输出流关联的 Socket 也会被关闭,所以一般不用关闭流,直接关闭 Socket 即可。

Socket 原理机制

  • 通信的两端都有 Socket
  • 网络通信其实就是 Socket 间的通信
  • 数据在两个 Socket 间通过 IO 传输

四种常见异常

以下四种类型异常都是继承于 IOException,所以很多之后直接弹出 IOException 即可。

  • UnkownHostException:主机名字或 IP 错误
  • ConnectException:服务器拒绝连接、服务器没有启动、超出队列数,拒绝连接等
  • SocketTimeoutException:连接超时
  • BindExceptionSocket 对象无法与指定的本地 IP 地址或端口绑定

Socket

构造方法

1
2
3
4
5
6
Socket(String host, int port)throws UnknownHostException, IOException
Socket(InetAddress address, int port) throws IOException
Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
...
private Socket(SocketAddress address, SocketAddress localAddr, boolean stream) throws 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
2
3
4
public ServerSocket() throws IOException   
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) 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 通信步骤

通信流程图

0038-socket-communication.png

大概流程如下:

  • 服务端 SocketServer 初始化
  • 服务端等待并接受客户端的连接 accept
  • 客户端 Socket 初始化
  • 数据通信
  • 关闭输入输出流,再关闭 Socket/SocketServer

0038-socket-socketServer-commu.png

服务端 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
2
SocketServer.java: public Socket accept() throws IOException;
SocketImpl.java: protected abstract void accept(SocketImpl s) 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
    5
    Socket.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

ServerSocketSocket 没有先后关闭的必然顺序。ServerSocket 先关闭,并不会影响当前已经连接的 Sokcet

TCP 通信示例

流程图

0038-socket-server-client.png

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
public class TestTCPSocketServer {

private static final String QUIT = "quit";
private static final String EXIT = "exit";

private volatile boolean isQuited = false;
private ServerSocket ss = null;
private Socket socket = null;
private Thread listenQuit;
private ListenRunnable listenRunnable;
private ExecutorService threadPool;

private class ListenRunnable implements Runnable{
private ServerSocket ss;

public ListenRunnable(ServerSocket ss){
this.ss = ss;
}

@Override
public void run() {
try {
// server exit.
BufferedReader ssRead = new BufferedReader(
new InputStreamReader(System.in));
System.out.println(Thread.currentThread().getName()
+ " listen server input...");
String ssInput = ssRead.readLine();
System.out.println(ssInput);
if (willQuit(ssInput)) {
ssRead.close();
isQuited = true;
}
} catch (IOException e){
System.err.println(e);
} finally {
if (isQuited) {
// 服务端退出时关闭线程池
if (threadPool != null && !threadPool.isShutdown()) {
threadPool.shutdown();
}

closeServerSocket(ss);
}
}
}
}

private class ClientCallable implements Callable<Void>{

private Socket socket;

public ClientCallable(Socket socket){
this.socket = socket;
}

@Override
public Void call() {
try {
String origHost = socket.getInetAddress().toString();
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
//PrintWriter out = new PrintWriter(
// socket.getOutputStream(), true);
String line = in.readLine();
while (line != null && !willQuit(line)) {
System.out.println(origHost + ", you input is : " + line);
line = in.readLine();
}

//out.println("you input is :" + line);
//out.close();
in.close();
} catch (IOException e){
System.err.println(e);
} finally {
closeSocket(socket);
}
return null;
}
}

private void closeSocket(Socket socket){
if (socket == null) return;

try {
System.out.println(socket.getInetAddress().toString() + " closed.");
socket.close();
} catch (IOException e){
System.err.println(e);
}
}

private void closeServerSocket(ServerSocket ss){
if (ss == null) return;

try {
System.out.println("Server closed");
ss.close();
} catch (IOException e){
System.err.println(e);
}
}

// 客户端和服务端,接受到 exit/quit 表示退出
private boolean willQuit(String str){
return str != null && (str.equalsIgnoreCase(QUIT)
|| str.equalsIgnoreCase(EXIT));
}

private void startServerDaemon(ServerSocket ss){
listenRunnable = new ListenRunnable(ss);
listenQuit = new Thread(listenRunnable);
listenQuit.setDaemon(true);
listenQuit.start();
}

private void run(){
try {
// 2. 初始化 ServerSocket,并绑定端口
ss = new ServerSocket(10000);
// 3. 启动后台线程监听服务端终端输入
startServerDaemon(ss);
System.out.println("serverSocket.getInetAddress: "
+ ss.getInetAddress());
System.out.println("serverSocket.getLocalSocketAddress: "
+ ss.getLocalSocketAddress());
System.out.println("serverSocket.getLocalPort: " + ss.getLocalPort());
System.out.println("The server is waiting you connect and input...");

while (!isQuited) {
// 4. 循环等待客户端连接
socket = ss.accept();
System.out.println("Socket: " + socket + " connected.");
// 5. 线程池为每个客户端连接分配一个线程独立处理双方通信
Callable<Void> client = new ClientCallable(socket);
threadPool.submit(client);
System.out.println("ReAccept...");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSocket(socket);
closeServerSocket(ss);
}
}

public TestTCPSocketServer() {
// 1. 初始化线程池
threadPool = Executors.newFixedThreadPool(20);
}

public static void main(String[] args) {
TestTCPSocketServer server = new TestTCPSocketServer();
server.run();
}
}

大致思路:

  • 初始化线程池,用于接受并执行客户端的请求任务
  • 创建 ServerSocket 对象,绑定监听端口
  • 开启后台线程,监听终端输入 exit/quit
  • 循环调用 accept() 方法,监听客户端请求
  • 接收到请求后,为每个客户端创建 Socket 专线连接
  • 线程池为每个客户端提供一个单独线程用于 Socket 通信
  • 服务器端继续等待新的客户端连接
  • 独立线程中,通过输入流读取客户端发送的请求信息,通过输出流向客户端发送响应信息
  • 客户端断开连接后,关闭相关资源,并关闭该 Socket,释放当前线程
  • 服务端接收到终端输入退出指令后,关闭线程池并关闭 ServerSocket

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class TestTCPSocketClient {
private static final String QUIT = "quit";
private static final String EXIT = "exit";

Socket socket = null;

public TestTCPSocketClient() {
try {
socket = new Socket("127.0.0.1", 10000);
//socket = new Socket("10.20.6.25", 10000);

System.out.println("Socket: " + socket);
System.out.println("socket.getRemoteSocketAddress: "
+ socket.getRemoteSocketAddress());
System.out.println("socket.getInetAddress: "
+ socket.getInetAddress());
System.out.println("socket.getPort: " + socket.getPort());
System.out.println("socket.getLocalSocketAddress: "
+ socket.getLocalSocketAddress());
System.out.println("socket.getLocalAddress: "
+ socket.getLocalAddress());
System.out.println("socket.getLocalPort: "
+ socket.getLocalPort());

// in = new BufferedReader(new InputStreamReader(
// socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream()
, true);
BufferedReader line = new BufferedReader(
new InputStreamReader(System.in));
System.out.println("read line! block...");
String read = line.readLine();
while (!willQuit(read)) {
out.println(read);
read = line.readLine();
}
System.out.println("read over!");

line.close();
out.close();
//in.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e){
System.err.println(e);
}
}
}
}

private boolean willQuit(String str){
return str != null && (str.equalsIgnoreCase(QUIT)
|| str.equalsIgnoreCase(EXIT));
}

public static void main(String[] args) {
new TestTCPSocketClient();
}
}

大致思路:

  • 创建 Socket 对象,指明需要连接的服务器的地址和端口号
  • 连接建立后,从终端读取输入信息
  • 通过输出流向服务器端发送读取到的信息
  • 通过输入流获取服务器响应的信息
  • 关闭相关资源,关闭 Socket

DatagramPacket

数据报(Datagram):将数据填充到 UDP 包中。数据报存放到 DatagramPacket 中,而 DatagramSocket 用来收发数据报。数据报的所有信息都包含在这个包中(包括发往的目标地址),Socket 只需要了解端口监听和发送。
UDP 这种模式并没有像 TCP 那样两台主机有唯一连接的概念,一个 Socket 会收发所有指定端口的数据,而不会关心对方是哪个主机。 DatagramSocket 可以从多个独立主机收发数据,与 TCP 不同,这个 Socket 并不会专用于某个连接。
TCPSocket 把网络连接看作是一个流,通过输入输出流来收发数据。但是 UDP 处理的总是单个数据报包,填充在数据报包中的所有数据会以一个包的形式发送,这些数据要么全部接受,要么全部丢弃。一个包与另一个包并不一定相关,而且无法确认先后顺序。
对于流必须提供数据的有序队列,而数据报会尽可能快的发送到接收方。

UDP 服务端通常不需要使用多线程,不会阻塞等待客户端的响应,除非为了应对大量耗时工作才会使用多线程。

UDP 数据报是基于 IP 数据报建立的,只向其底层 IP 数据添加了很少的内容(8 个字节的首部信息)。UDP 包中数据的理论长度为 65507 个字节,但实际上很多平台限制往往是 8KB (8192 字节),大多数情况下更大的包会被简单的截取为 8KB。并且为了保证安全性,UDP 包的数据部分应该尽量少于 512 字节。

构造方法

1
2
3
4
5
6
7
8
9
10
// 接受数据报
public DatagramPacket(byte buf[], int offset, int length) {...}
public DatagramPacket(byte buf[], int length) {...}
// 发送数据报
public DatagramPacket(byte buf[], int offset, int length,
InetAddress address, int port) {...}
public DatagramPacket(byte buf[], int offset, int length, SocketAddress address) {...}
public DatagramPacket(byte buf[], int length,
InetAddress address, int port) {...}
public DatagramPacket(byte buf[], int length, SocketAddress address) {...}

注意:DatagramPacket 虽然都是构造方法,但是有 2 个是接收数据报的构造方法,剩余 4 个是发送数据报的构造方法。普通情况下构造方法主要用于不同对象提供不同的类型信息,而不是像数据报这样提供不同的功能对象。

参数解析:

  • buf[]
    保存接收或发送数据的数组,也就是不管是发送还是接受拿到的都是字节数组。
  • length
    数组中用于接受或发送数据的长度。
  • offset
    数组的偏移量,默认为 0。
  • InetAddress
    发送数据时接收方的 IP 地址。
  • port
    发送数据时接收方的端口。
  • SocketAddress
    发送数据时接收方的地址和端口

get 方法

get 方法可以获取构造方法中传进来的所有参数信息。

1
2
3
4
5
6
7
8
9
10
// 获取 IP 地址和端口
public synchronized InetAddress getAddress() {...}
public synchronized int getPort(){...}
public synchronized SocketAddress getSocketAddress() {...}
// 获取数据数组
public synchronized byte[] getData() {...}
// 数组偏移量
public synchronized int getOffset() {...}
// 数组长度
public synchronized int getLength() {...}

set 方法

set 方法可以在构造方法创建数据报后,改变所有的数据报信息,相当于重新创建了一个数据报。因为 DatagramPacket 对象的重复创建和垃圾回收影响性能,所以重用对象比构造对象快的多。

1
2
3
4
5
6
7
8
// 设置 IP 地址和端口
public synchronized void setAddress(InetAddress iaddr) {...}
public synchronized void setPort(int iport) {...}
public synchronized void setSocketAddress(SocketAddress address) {...}
// 设置数据数组,偏移量,长度
public synchronized void setData(byte[] buf) {...}
public synchronized void setData(byte[] buf, int offset, int length) {...}
public synchronized void setLength(int length) {...}

注意:所有的 set 方法都加了同步锁。

DatagramSocket

要收发数据报,需要先打开一个 DatagramSocket,而所有的 DatagramSocket 必须绑定一个本地端口,这个端口用来监听收发的数据,并写入数据报的首部。
DatagramSocket 只存储本地地址和端口,所有的远程地址和端口都在 DatagramPacket 中,所以 DatagramSocket 可以同时和多台主机收发数据(只要 DatagramPacket 不同就可以了)。

构造方法

1
2
3
4
5
6
public class DatagramSocket implements java.io.Closeable {
public DatagramSocket() throws SocketException {...}
public DatagramSocket(int port) throws SocketException {...}
public DatagramSocket(int port, InetAddress laddr) throws SocketException {...}
public DatagramSocket(SocketAddress bindaddr) throws SocketException {...}
}

收发数据报

1
2
public void send(DatagramPacket p) throws IOException  {...}
public synchronized void receive(DatagramPacket p) throws IOException {...}

send 用来发送数据报;receive 加了同步锁,并且会阻塞当前线程,直到有数据到达,接收一个数据报。

常见 API

1
2
3
4
5
6
7
8
9
10
11
// 关闭 socket
public void close() {...}
// 获取源 IP 和端口
public SocketAddress getLocalSocketAddress() {...}
public InetAddress getLocalAddress() {...}
public int getLocalPort() {...}
// 连接指定地址和端口,仅能和这台主机通信,其他主机将断开
public void connect(SocketAddress addr) throws SocketException {...}
public void connect(InetAddress address, int port) {...}
// 断开连接,恢复可以和多台主机收发数据
public void disconnect() {...}

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 通信示例

流程图

0038-udp-server-client-commu.jpg

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TestDayTimeUDPServer {

private static final int PORT = 13;
private static final int CAPACITY = 1024;
private static final Logger audit = Logger.getLogger("requests");
private static final Logger errors = Logger.getLogger("errors");

public static void main(String[] args) {
DatagramSocket datagramSocket = null;
try {
datagramSocket = new DatagramSocket(PORT);
System.out.println("Source: " + datagramSocket.getLocalSocketAddress());
while (true){
try {
DatagramPacket request = new DatagramPacket(
new byte[CAPACITY], CAPACITY);
datagramSocket.receive(request);

String dayTime = new Date().toString();
byte[] data = dayTime.getBytes("US-ASCII");
DatagramPacket response = new DatagramPacket(data, data.length,
request.getAddress(), request.getPort());
datagramSocket.send(response);
System.out.println("Destination: " + request.getSocketAddress());
audit.info(dayTime + " " + request.getAddress());
} catch (IOException e){
errors.log(Level.SEVERE, e.getMessage(), e);
}
}
} catch (IOException e){
errors.log(Level.SEVERE, e.getMessage(), e);
}
}
}

大致思路:

  • 创建 DatagramSocket,绑定端口号
  • 创建 DatagramPacket,指定目标地址和端口
  • 阻塞等待接收客户端发送的数据
  • 向客户端发送响应数据

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TestUDPGetDayTime {

// private static final String HOSTNAME = "time.nist.gov";
// private static final String HOSTNAME = "129.6.15.28";
private static final String HOSTNAME = "127.0.0.1";
private static final int PORT = 38910;
private static final int DELAY_TIME = 10000;
private static final int CAPACITY = 1024;

public static void main(String[] args) {
DatagramSocket datagramSocket = null;
try {
datagramSocket = new DatagramSocket();
datagramSocket.setSoTimeout(DELAY_TIME);
InetAddress host = InetAddress.getByName(HOSTNAME);
DatagramPacket request = new DatagramPacket(new byte[1], 1, host, PORT);
DatagramPacket response = new DatagramPacket(new byte[CAPACITY], CAPACITY);
datagramSocket.send(request);
System.out.println("Source: " + datagramSocket.getLocalSocketAddress());
System.out.println("Destination: " + request.getSocketAddress());
// block.
datagramSocket.receive(response);
String dayTime = new String(response.getData(), 0,
response.getLength(), "US-ASCII");
System.out.println(dayTime);
} catch (IOException e){
System.err.println(e);
} finally {
if (datagramSocket != null) {
datagramSocket.close();
}
}
}
}

大致思路:

  • 定义发送信息,并存储到数组中
  • 创建 DatagramPacket,指定目标地址和端口,数据数组
  • 创建 DatagramSocket,绑定本地端口
  • 向服务端请求数据
  • 从服务端获取响应数据

参考文档

  1. Java Socket编程
  2. Socket和ServerSocket学习笔记
  3. Java网络socket编程详解
  4. TCP数据段格式+UDP数据段格式详解