在本 Java 网络编程教程中,您将学习如何开发套接字服务器程序来实现功能齐全的网络客户端/服务器应用程序。您还将学习如何创建多线程服务器。
首先,让我们了解一下工作流和 API。
1.ServerSocket API
该ServerSocket的类用来实现服务器程序。以下是开发服务器程序的典型步骤:
1.创建一个服务端socket并绑定到一个特定的端口号
2. 监听来自客户端的连接并接受它。这导致为连接创建客户端套接字。
3.通过从客户端socket获取的InputStream从客户端读取数据。
4. 通过客户端套接字的OutputStream向客户端发送数据。
5. 关闭与客户端的连接。
根据服务器和客户端之间商定的协议,步骤 3 和 4 可以重复多次。
可以对每个新客户端重复步骤 1 到 5。每个新连接都应该由一个单独的线程处理。
让我们详细了解每个步骤。
创建服务器套接字:
使用以下构造函数之一创建ServerSocket类的新对象:
- ServerSocket(int port) : 创建一个绑定到指定端口号的服务器套接字。排队的传入连接的最大数量设置为 50(当队列已满时,拒绝新连接)。
- ServerSocket(int port, int backlog):创建一个绑定到指定端口号的服务器套接字,并且最大排队连接数由backlog 参数指定。
- ServerSocket(int port, int backlog, InetAddress bindAddr):创建服务器套接字并将其绑定到指定的端口号和本地 IP 地址。
那么什么时候使用哪个?
对少量排队连接(少于 50 个)和任何可用的本地 IP 地址使用第一个构造函数。
如果要明确指定排队请求的最大数量,请使用第二个构造函数。
如果要明确指定要绑定的本地 IP 地址(以防计算机有多个 IP 地址),请使用第三个构造函数。
当然,第一个构造函数更适合简单使用。例如,以下代码行创建一个服务器套接字并将其绑定到端口号 6868:
1
|
ServerSocket serverSocket = new ServerSocket( 6868 );
|
请注意,如果在打开套接字时发生 I/O 错误,这些构造函数可能会抛出IOException,因此您必须捕获或重新抛出它。
监听连接:
一旦ServerSocket的实例被创建,调用accept()方法开始监听传入的客户端请求:
1
|
Socket socket = serverSocket.accept();
|
请注意,accept()方法会阻塞当前线程,直到建立连接。并且连接由返回的Socket对象表示。
从客户端读取数据:
一旦一个套接字对象被返回,你可以使用它的InputStream来读取客户端发送这样的数据:
1
|
InputStream input = socket.getInputStream();
|
该的InputStream可以读取处于较低水平的数据:读取的字节数组。因此,如果您想在更高级别读取数据,请将其包装在InputStreamReader 中以将数据作为字符读取:
1
2
|
InputStreamReader reader = new InputStreamReader(input);
int character = reader.read(); // reads a single character
|
您还可以将InputStream包装在BufferedReader 中以将数据读取为 String,以便更方便:
1
2
|
BufferedReader reader = new BufferedReader( new InputStreamReader(input));
String line = reader.readLine(); // reads a line of text
|
向客户端发送数据:
使用与Socket关联的OutputStream向客户端发送数据,例如:
1
|
OutputStream output = socket.getOutputStream();
|
由于OutputStream仅提供低级方法(将数据写入字节数组),您可以将其包装在PrintWriter 中以文本格式发送数据,例如:
1
2
|
PrintWriter writer = new PrintWriter(output, true );
writer.println(“This is a message sent to the server”);
|
参数true表示编写器在每次方法调用后刷新数据(自动刷新)。
关闭客户端连接:
调用客户端Socket上的close()方法终止与客户端的连接:
1
|
socket.close();
|
此方法还关闭套接字的InputStream和OutputStream,如果关闭套接字时发生 I/O 错误,它可以抛出IOException。
我们建议您使用try-with-resource 结构,这样您就不必编写代码来显式关闭套接字。
当然,服务器仍在运行,用于为其他客户端提供服务。
终止服务器:
服务器应该始终运行,等待来自客户端的传入请求。如果由于某些原因必须停止服务器,请调用ServerSocket 实例上的close()方法:
1
|
serverSocket.close();
|
当服务器停止时,所有当前连接的客户端都将断开连接。
该ServerSocket的类还提供了其他的方法,你可以在它的Javadoc咨询这里。
实现多线程服务器:
所以基本上,服务器程序的工作流程是这样的:
1
2
3
4
5
6
7
8
|
ServerSocket serverSocket = new ServerSocket(port);
while ( true ) {
Socket socket = serverSocket.accept();
// read data from the client
// send data to the client
}
|
在一段时间(真)循环用于允许服务器一直运行下去,一直等待来自客户端的连接。但是,有一个问题:一旦连接了第一个客户端,如果服务器忙于为第一个客户端提供服务,则它可能无法处理后续客户端。
因此,为了解决这个问题,使用了线程:每个客户端套接字由一个新线程处理。服务器的主线程只负责监听和接受新的连接。因此,工作流被更新以实现多线程服务器,如下所示:
1
2
3
4
5
|
while ( true ) {
Socket socket = serverSocket.accept();
// create a new thread to handle client socket
}
|
您将在下面的示例中清楚地了解。
2. Java 服务器套接字示例 #1:时间服务器
以下程序演示了如何实现一个简单的服务器,为每个新客户端返回当前日期时间。这是代码:
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.*;
import java.net.*;
import java.util.Date;
/**
* This program demonstrates a simple TCP/IP socket server.
*
* @author www.codejava.net
*/
public class TimeServer {
public static void main(String[] args) {
if (args.length < 1 ) return ;
int port = Integer.parseInt(args[ 0 ]);
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println( "Server is listening on port " + port);
while ( true ) {
Socket socket = serverSocket.accept();
System.out.println( "New client connected" );
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true );
writer.println( new Date().toString());
}
} catch (IOException ex) {
System.out.println( "Server exception: " + ex.getMessage());
ex.printStackTrace();
}
}
}
|
运行此服务器程序时需要指定端口号,例如:
1
|
java TimeServer 6868
|
这使服务器在端口号 6868 上侦听客户端请求。您将看到服务器的输出:
1
|
Server is listening on port 6868
|
下面的代码是一个客户端程序,它简单地连接到服务器并打印接收到的数据,然后终止:
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
|
import java.net.*;
import java.io.*;
/**
* This program demonstrates a simple TCP/IP socket client.
*
* @author www.codejava.net
*/
public class TimeClient {
public static void main(String[] args) {
if (args.length < 2 ) return ;
String hostname = args[ 0 ];
int port = Integer.parseInt(args[ 1 ]);
try (Socket socket = new Socket(hostname, port)) {
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader( new InputStreamReader(input));
String time = reader.readLine();
System.out.println(time);
} catch (UnknownHostException ex) {
System.out.println( "Server not found: " + ex.getMessage());
} catch (IOException ex) {
System.out.println( "I/O error: " + ex.getMessage());
}
}
}
|
要运行此客户端程序,您必须指定服务器的主机名/IP 地址和端口号。如果客户端与服务器在同一台计算机上,请键入以下命令以运行它:
1
|
java TimeClient localhost 6868
|
然后你会在服务器程序中看到一个新的输出,表明客户端已连接:
1
|
New client connected
|
您应该会看到客户端的输出:
1
|
Mon Nov 13 11:00:31 ICT 2017
|
这是从服务器返回的日期时间信息。然后客户端终止,服务器仍在运行,等待新的连接。就这么简单。
3. Java Socket Server Example #2:反向服务器(单线程)
接下来,让我们看一个更复杂的套接字服务器示例。以下服务器程序以相反的形式回显从客户端发送的任何内容(因此名称为ReverseServer)。这是代码:
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
|
import java.io.*;
import java.net.*;
/**
* This program demonstrates a simple TCP/IP socket server that echoes every
* message from the client in reversed form.
* This server is single-threaded.
*
* @author www.codejava.net
*/
public class ReverseServer {
public static void main(String[] args) {
if (args.length < 1 ) return ;
int port = Integer.parseInt(args[ 0 ]);
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println( "Server is listening on port " + port);
while ( true ) {
Socket socket = serverSocket.accept();
System.out.println( "New client connected" );
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader( new InputStreamReader(input));
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true );
String text;
do {
text = reader.readLine();
String reverseText = new StringBuilder(text).reverse().toString();
writer.println( "Server: " + reverseText);
} while (!text.equals( "bye" ));
socket.close();
}
} catch (IOException ex) {
System.out.println( "Server exception: " + ex.getMessage());
ex.printStackTrace();
}
}
}
|
如您所见,服务器继续为客户端提供服务,直到它说“再见”为止。使用以下命令运行此服务器程序:
1
|
java ReverseServer 9090
|
服务器启动并运行,等待来自客户端的传入请求:
1
|
Server is listening on port 9090
|
现在,让我们创建一个客户端程序。以下程序连接到服务器,读取用户的输入并打印来自服务器的响应。这是代码:
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
|
import java.net.*;
import java.io.*;
/**
* This program demonstrates a simple TCP/IP socket client that reads input
* from the user and prints echoed message from the server.
*
* @author www.codejava.net
*/
public class ReverseClient {
public static void main(String[] args) {
if (args.length < 2 ) return ;
String hostname = args[ 0 ];
int port = Integer.parseInt(args[ 1 ]);
try (Socket socket = new Socket(hostname, port)) {
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true );
Console console = System.console();
String text;
do {
text = console.readLine( "Enter text: " );
writer.println(text);
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader( new InputStreamReader(input));
String time = reader.readLine();
System.out.println(time);
} while (!text.equals( "bye" ));
socket.close();
} catch (UnknownHostException ex) {
System.out.println( "Server not found: " + ex.getMessage());
} catch (IOException ex) {
System.out.println( "I/O error: " + ex.getMessage());
}
}
}
|
如您所见,此客户端程序一直在运行,直到用户键入“再见”为止。使用以下命令运行它:
1
|
java ReverseClient localhost 9090
|
然后它会要求您输入一些文本:
1
|
Enter text:_
|
输入一些东西,说“你好”,你应该看到服务器的响应是这样的:
1
2
3
|
Enter text: Hello
Server: olleH
Enter text:_
|
您会看到服务器响应“Server: olleH”,其中“olledH”是“Hello”的逆向形式。添加文本“服务器:”以明确区分客户端消息和服务器消息。客户端程序仍在运行,要求输入和打印服务器的响应,直到您键入“再见”以终止它。
保持第一个客户端程序运行,然后启动一个新的客户端程序。在第二个客户端程序中,您将看到它要求输入然后永远挂起。为什么?
因为服务端是单线程的,在忙着服务第一个客户端的时候,后面的客户端就被阻塞了。
让我们在下一个例子中看看如何解决这个问题。
4. Java Socket Server Example #3:反向服务器(多线程)
修改服务器的代码以在新线程中处理每个套接字客户端,如下所示:
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.*;
import java.net.*;
/**
* This program demonstrates a simple TCP/IP socket server that echoes every
* message from the client in reversed form.
* This server is multi-threaded.
*
* @author www.codejava.net
*/
public class ReverseServer {
public static void main(String[] args) {
if (args.length < 1 ) return ;
int port = Integer.parseInt(args[ 0 ]);
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println( "Server is listening on port " + port);
while ( true ) {
Socket socket = serverSocket.accept();
System.out.println( "New client connected" );
new ServerThread(socket).start();
}
} catch (IOException ex) {
System.out.println( "Server exception: " + ex.getMessage());
ex.printStackTrace();
}
}
}
|
你看,服务器为每个套接字客户端创建一个新的ServerThread:
1
2
3
4
5
6
|
while ( true ) {
Socket socket = serverSocket.accept();
System.out.println( "New client connected" );
new ServerThread(socket).start();
}
|
该ServerThread类的实现如下:
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
|
import java.io.*;
import java.net.*;
/**
* This thread is responsible to handle client connection.
*
* @author www.codejava.net
*/
public class ServerThread extends Thread {
private Socket socket;
public ServerThread(Socket socket) {
this .socket = socket;
}
public void run() {
try {
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader( new InputStreamReader(input));
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true );
String text;
do {
text = reader.readLine();
String reverseText = new StringBuilder(text).reverse().toString();
writer.println( "Server: " + reverseText);
} while (!text.equals( "bye" ));
socket.close();
} catch (IOException ex) {
System.out.println( "Server exception: " + ex.getMessage());
ex.printStackTrace();
}
}
}
|
如您所见,我们只是将要执行的处理代码移动到一个单独的线程中,在run()方法中实现。
现在让我们运行这个新的服务器程序并运行几个客户端程序,你会看到上面的问题已经解决了。所有客户端运行顺利。
让我们以不同的方式试验本课中的示例:运行多个客户端,在本地计算机上测试,并在不同的计算机上测试(服务器在一台机器上运行,客户端在另一台机器上运行)。
API参考: