17.3.3 使用Socket进行通信

17.3.3 使用Socket进行通信

客户端通常可以使用Socket的构造器来连接到指定服务器,Socket通常可以使用如下两个构造器。

本地主机只有一个IP地址的情况

方法 描述
Socket(String host, int port) 创建连接到指定远程主机、远程端口Socket,该构造器没有指定本地地址、本地端口,默认使用本地主机的默认IP地址,默认使用系统动态分配的端口。
Socket(InetAddress address, int port) Creates a stream socket and connects it to the specified port number at the specified IP address.

适用于本地主机有多个IP地址的情况

方法 描述
Socket(String host, int port, InetAddress localAddr, int localPort) 创建连接到指定远程主机、远程端口Socket,并指定本地IP地址和本地端口,适用于本地主机有多个IP地址的情形
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) Creates a socket and connects it to the specified remote address on the specified remote port.

上面两个构造器中指定远程主机时既可使用InetAddress来指定,也可直接使用String对象来指定,但程序通常使用String对象(如192.168.2.23)来指定远程IP地址。当本地主机只有一个IP地址时,使用第一类Socket构造器更为简单。如下代码所示。

1
2
3
//创建连接到本机、30000端口的`Socket`
Socket socket = new Socket("127.0.0.1" , 30000);
//下面就可以使用`Socket`进行通信了

当程序执行上面Socket构造器时,该代码将会连接到指定服务器,让服务器端的ServerSocketaccept()方法向下执行,于是服务器端和客户端就产生一对互相连接的Socket

上面程序连接到“远程主机”的地址使用的是127.0.0.1,这个IP地址是一个特殊的地址,它总是代表本机的IP地址。因为本书的示例程序的服务器端、客户端都是在本机运行的,所以Socket连接的远程主机的IP地址使用127.0.0.1

获取Socket的输入输出流

当客户端、服务器端产生了对应的Socket之后,就得到了如图17.4所示的通信示意图,程序无须再区分服务器端、客户端,而是通过各自的Socket进行通信。Socket提供了如下两个方法来获取输入流和输出流。

方法 描述
InputStream getInputStream() 返回该Socket对象对应的输入流,让程序通过该输入流从Socket中取出数据。
OutputStream getOutputStream() 返回该Socket对象对应的输岀流,让程序通过该输出流向Socket中输出数据。

看到这两个方法返回的InputStreamOutputStream,读者应该可以明白Java在设计IO体系上的苦心了——不管底层的IO流是怎样的节点流:文件流也好,网络Socket产生的流也好,程序都可以将其包装成处理流,从而提供更多方便的处理。下面以一个最简单的网络通信程序为例来介绍基于TCP协议的网络通信。

程序示例 最简单的TCP通信

下面的服务器端程序非常简单,它仅仅建立ServerSocket监听,并使用Socket获取输出流输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.net.*;
import java.io.*;

public class Server {
public static void main(String[] args) throws IOException {
// 创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket serverSocket = new ServerSocket(30000);
// 采用循环不断接受来自客户端的请求
int count = 0;
while (true) {
// 每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket server = serverSocket.accept();
// 将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(server.getOutputStream());
// 进行普通IO操作
ps.println("你好! 你是第"+(count++)+"号客户端");
// 关闭输出流,关闭Socket
ps.close();
server.close();
}
}
}

下面的客户端程序也非常简单,它仅仅使用Socket建立与指定IP地址、指定端口的连接,并使用Socket获取输入流读取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.net.*;
import java.io.*;

public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 30000); // ①
// 将Socket对应的输入流包装成BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 进行普通IO操作
String line = br.readLine();
System.out.println("来自服务器的数据:" + line);
// 关闭输入流、socket
br.close();
socket.close();
}
}

上面程序中①号代码是使用SocketServerSocket建立网络连接的代码,接下来代码通过Socket获取输入流、输岀流进行通信的代码。通过程序不难看出,一旦使用ServerSocket,Socket建立网络连接之后,程序通过网络通信与普通IO并没有太大的区别。
先运行程序中的Server类,将看到服务器一直处于等待状态,因为服务器使用了死循环来接收来自客户端的请求;
再运行Client类,将看到程序输岀:“来自服务器的数据:来自服务器的数据:你好! 你是第0号客户端”,这表明客户端和服务器端通信成功。
上面程序为了突出通过ServerSocketSocket建立连接,并通过底层IO流进行通信的主题,程序没有进行异常处理,也没有使用finally块来关闭资源

设置超时时间

在实际应用中,程序可能不想让执行网络连接、读取服务器数据的进程一直阻塞,而是希望当网络连接、读取操作超过合理时间之后,系统自动认为该操作失败,这个合理时间就是超时时长。Socket对象提供了一个setSoTimeout(int timeout)方法来设置超时时长。如下代码片段所示:

1
2
3
Socket s=new Socket("127.0.0.1",30000);
//设置10秒之后即认为超时
s.setsoTimeout(10000);

Socket对象指定了超时时长之后,如果在使用Socket进行读、写操作完成之前超出了该时间限制,那么这些方法就会抛出SocketTimeoutException异常,程序可以对该异常进行捕获,并进行适当处理。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
try{
//使用 Scanner来读取网络输入流中的数据
Scanner scan = new Scanner(s.getInputstream());
//读取一行字符
string line = scan.nextline();
}
//捕获 SocketTimeoutException异常
catch(SocketTimeoutException ex)
{
//对异常进行处理
}

Socket连接服务器时指定超时时长

假设程序需要Socket连接服务器时指定超时时长,即经过指定时间后,如果该Socket还未连接到远程服务器,则系统认为该Socket连接超时。但Socket的所有构造器里都没有提供指定超时时长的参数,所以程序应该先创建一个无连接的Socket,再调用Socketconnect()方法来连接远程服务器而connec方法就可以接收一个超时时长参数。如下代码所示。

1
2
3
4
//创建一个无连接的 Socket
Socket s= new.Socket());
//让该 Socket连接到远程服务器,如果经过10秒还没有连接上,则认为连接超时
s.connect(new InetSocketAddress(host, port), 10000);