Java漫游笔记-11-网络编程

TCP 编程

Java 为 TCP 协议提供了两个类:ServerSocketSocket 类。
编程的步骤大致如下:

  1. 创建服务器套接字 ServerSocket ,并将其绑定到特定地址(IP 地址和端口号)。
  2. 调用 accept() 方法,侦听并接受到此套接字的连接。
  3. 创建客户端套接字 Socket ,并将其连接到指定的地址。
  4. 连接建立后,Socket 可以通过 getOutputStream() 获取输出流,向服务端发送数据。同时,ServerSocket 则可以通过 getInputStream() 获取输入流,来读取客户端的数据。
  5. 类似的,ServerSocket 可以通过 getOutputStream() 获取输出流,向客户端发送数据。Socket 也可以通过 getInputStream() 获取输入流,来读取服务器的数据。
  6. 最后,当然还需要关闭连接。

服务器端代码

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
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {

public static void main(String[] args) {
// 绑定的端口号
final int PORT = 12345;
ServerSocket server = null;
try {
// 创建服务器套接字
server = new ServerSocket(PORT);
// 持续监听
while (true) {
// 接受到此套接字的连接
Socket client = server.accept();

try (BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(client.getOutputStream()));) {
// 读取客户端的数据
String data;
while ((data = in.readLine()) != null) {
System.out.println("from client["
+ client.getRemoteSocketAddress().toString() + "] -> "
+ data);
// 向客户端发送数据
out.write(data + " received");
out.newLine();
out.flush();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server != null) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

}

客户端代码

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
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class TCPClient {

public static void main(String[] args) {
// 连接到指定 IP 地址的指定端口号
final String IP = "127.0.0.1";
final int PORT = 12345;
Socket client = null;
try {
client = new Socket(IP, PORT);
try (BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(client.getOutputStream()));
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));) {
// 向服务器发送数据
out.write(String.valueOf(Math.random()));
out.newLine();
out.flush();

// 读取服务器的数据
String data;
while ((data = in.readLine()) != null) {
System.out.println("from server["
+ client.getRemoteSocketAddress().toString()
+ "] -> " + data);
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

}

并发

以上的代码虽然允许多个客户端与服务器建立连接,但是服务端处理客户端的连接请求是同步进行的。也就是说,服务器每次接收到来自客户端的连接请求后,都要先跟当前的客户端通信完成后,才能接着处理下一个客户端的连接请求。
在实际工作中,一个服务器往往需要同时处理来自多个客户端的连接请求。
这时候就需要引入多线程,一旦监听到新的客户端请求连接,就应该启动(或者从线程池中获取)一个新的线程来响应请求。

新增线程类:

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
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class TCPThread implements Runnable {

private Socket client;

public TCPThread(Socket client) {
this.client = client;
}

@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(
client.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
client.getOutputStream()));) {
// 读取客户端的数据
String data;
while ((data = in.readLine()) != null) {
System.out.println("from client["
+ client.getRemoteSocketAddress().toString() + "] -> "
+ data);
// 向客户端发送数据
out.write(data + " received");
out.newLine();
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}

}

服务器端修改后的代码:

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
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {

public static void main(String[] args) {
// 绑定的端口号
final int PORT = 12345;
ServerSocket server = null;
try {
// 创建服务器套接字
server = new ServerSocket(PORT);
// 持续监听
while (true) {
// 接受到此套接字的连接
Socket client = server.accept();
// 启动新的线程
new Thread(new TCPThread(client)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server != null) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

}

UDP 编程

Java 为 UDP 协议提供了:DatagramSocketDatagramPacket 类。
编程的步骤大致如下:

  1. 创建接收端的数据报套接字 DatagramSocket ,并将其绑定到指定的地址。
  2. 监听端口,通过 DatagramSocketreceive() 方法接收数据报文 DatagramPacket
  3. 创建发送端的数据报套接字,然后通过 send() 方法来发送 DatagramPacket
  4. 最后,关闭连接。

服务器段代码

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
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UDPServer {

public static void main(String[] args) {
final int PORT = 54321;
DatagramSocket server = null;
try {
// 创建数据报套接字
server = new DatagramSocket(PORT);
// 创建接收包
byte buf[] = new byte[1024];
DatagramPacket data = new DatagramPacket(buf, buf.length);
// 持续监听
while (true) {
// 接收数据报包
server.receive(data);
System.out.println("from client["
+ data.getAddress().toString() + ":" + data.getPort()
+ "] -> " + new String(data.getData()));
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭连接
if (server != null) {
server.close();
}
}
}

}

客户端代码

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
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;

public class UDPClient {

public static void main(String[] args) {
final int PORT = 54321;
DatagramSocket client = null;
try {
// 创建数据报套接字,并将其绑定到本地主机上任何可用的端口
client = new DatagramSocket();
// 创建发送包
InetAddress address = InetAddress.getByName("localhost"); // 127.0.0.1
byte buf[] = String.valueOf(Math.random()).getBytes();
DatagramPacket data = new DatagramPacket(buf, buf.length, address,
PORT);
// 发送数据报包
client.send(data);
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭连接
if (client != null) {
client.close();
}
}
}

}

总结

  • 计算机网络的真正“用户”是运行在主机上的应用程序。
  • 信息是由程序创建的数据(字节序列)。
  • 协议是相互通信的应用程序间达成的一种约定,它规定了数据交换的方式。设计一组协议往往是为了在特定条件下解决某一类问题。
  • TCP/IP 是互联网使用的协议族,它主要包含:IP 协议(网络层)、 TCP 协议(传输层) 和 UDP 协议(传输层)。
  • 一个互联网地址(InetAddress)代表了一台主机与底层通信信道的连接,简单来说就是一个网络接口。举例来说,IP 地址相当于一栋大楼的门牌号,而端口号则是这个大楼里面的房间号。
  • 套接字(Socket)是应用程序和传输层之间的一个抽象层,应用程序通过它来发送和接收数据,与网络中其他应用程序进行通信。套接字在类型上,又分为:流套接字和数据报套接字。
  • TCP 是面向连接的协议,好比打电话,通信两端需要建立连接。
  • 而 UDP 不是连接型协议,更像是邮政服务,你只需要为每个包裹指定目标地址即可。
    因此,我们可以把 UDP 套接字看成是一个信箱,可以向不同地址发送信息,也可以接收从其它地址发过来的信息。UDP 套接字会尽可能(best-effort)地传输信息,但是并不保证一定送到目的地,也不保证严格按照顺序来发送。因此,使用 UDP 要做好信息丢失和重排的准备。
  • 还有一点需要注意的:TCP 是面向(字节)流的,而 UDP 则是面向数据报的
    另一种更复杂的说法是:UDP 每次收发的报文都会保留消息边界
    这怎么理解呢?
    在我看来,保留消息边界,就是要划清界限的意思,也就是说,每个数据报都会和其它的数据报划清界限。换言之,数据报之间相互独立。
    这个特性,决定了 UDP 使用的数据报套接字时,每一次调用 receive() 方法只能接收一次 send() 方法发送的数据报文。
    再回过头来看 TCP ,由于 TCP 是面向流的,一串数据包就像一条河流一样,没有明确的边界,因此可能会出现所谓的“粘包”现象,而导致解析错误。在 TCP 编程时务必要记住:从一端的输出流调用一次 write() 方法写入数据,到另一端的输入流调用一次 read() 方法读取数据,二者没有必然的对应关系。如果接收端的缓冲区足够大,调用一次 read() 可能可以读取发送方多次 write() 写入的数据,反之,如果缓冲区不够大,那么就只能 read() 到对方一次 write() 的部分数据。要解决这个问题,可以强制立即发送数据,也可以在发送前进行“封包”,再在接收的时候进行“拆包”
  • TCP 服务器是一种迭代服务器,会按顺序处理客户端请求,采用多线程可以实现并行服务器。