JAVA套接字(Socket)101七天系列—第四天【一个简单示例】

时间:2021-10-15 20:43:14

一个简单示例
 1. 背景

我们将在本部分讨论的示例将阐明在 Java 代码中如何使用 SocketServerSocket。客户机用Socket 连接到服务器。服务器用ServerSocket 在端口 3000 侦听。客户机请求服务器 C: 驱动器上的文件内容。

为清楚起见,我们把示例分解成客户机端和服务器端。最后我们将把它们组合起来以使您能看到整体模样。

我们在使用 JDK 1.2 的 IBM VisualAge for Java 3.5 上开发这些代码。要自己创建这个示例,您应有完好的 JDK 1.1.7 或更高版本。客户机和服务器将只在一台机器上运行,所以您不必担心是否有一个可用的网络。


 2. 创建 RemoteFileClient 类

这里是 RemoteFileClient 类的结构:

import java.io.*;
import java.net.*;

public class RemoteFileClient {
protected String hostIp;
protected int hostPort;
protected BufferedReader socketReader;
protected PrintWriter socketWriter;

public RemoteFileClient(String aHostIp, int aHostPort) {
hostIp = aHostIp;
hostPort = aHostPort;
}
public static void main(String[] args) {
}
public void setUpConnection() {
}
public String getFile(String fileNameToGet) {
}
public void tearDownConnection() {
}
}


 

首先我们导入 java.netjava.iojava.net 包为您提供您需要的套接字工具。java.io 包为您提供对流进行读写的工具,这是您与 TCP 套接字通信的唯一途径。

我们给我们的类实例变量以支持对套接字流的读写和存储我们将连接到的远程主机的详细信息。

我们类的构造器有两个参数:远程主机的 IP 地址和端口号各一个,而且构造器将它们赋给实例变量。

我们的类有一个 main() 方法和三个其它方法。稍后我们将探究这些方法的细节。现在您只需知道setUpConnection() 将连接到远程服务器,getFile() 将向远程服务器请求fileNameToGet 的内容以及tearDownConnection() 将从远程服务器上断开。


 3. 实现 main()

这里我们实现 main() 方法,它将创建 RemoteFileClient 并用它来获取远程文件的内容,然后打印结果:

public static void main(String[] args) {
RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1", 3000);
remoteFileClient.setUpConnection();
String fileContents =
remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");
remoteFileClient.tearDownConnection();

System.out.println(fileContents);
}


 

main() 方法用主机的 IP 地址和端口号实例化一个新RemoteFileClient(客户机)。然后,我们告诉客户机建立一个到主机的连接(稍后有更详细的讨论)。接着,我们告诉客户机获取主机上一个指定文件的内容。最后,我们告诉客户机断开它到主机的连接。我们把文件内容打印到控制台,只是为了证明一切都是按计划进行的。


4. 建立连接

这里我们实现 setUpConnection() 方法,它将创建我们的 Socket 并让我们访问该套接字的流:

public void setUpConnection() {
try {
Socket client = new Socket(hostIp, hostPort);

socketReader = new BufferedReader(
new InputStreamReader(client.getInputStream()));
socketWriter = new PrintWriter(client.getOutputStream());

} catch (UnknownHostException e) {
System.out.println("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);
} catch (IOException e) {
System.out.println("Error setting up socket connection: " + e);
}
}


 

setUpConnection() 方法用主机的 IP 地址和端口号创建一个Socket

Socket client = new Socket(hostIp, hostPort);


 

我们把 SocketInputStream 包装进BufferedReader 以使我们能够读取流的行。然后,我们把SocketOutputStream 包装进PrintWriter 以使我们能够发送文件请求到服务器:

socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
socketWriter = new PrintWriter(client.getOutputStream());


 

请记住我们的客户机和服务器只是来回传送字节。客户机和服务器都必须知道另一方即将发送的是什么以使它们能够作出适当的响应。在这个案例中,服务器知道我们将发送一条有效的文件路径。

当您实例化一个 Socket 时,将抛出UnknownHostException。这里我们不特别处理它,但我们打印一些信息到控制台以告诉我们发生了什么错误。同样地,当我们试图获取SocketInputStreamOutputStream 时,如果抛出了一个一般IOException,我们也打印一些信息到控制台。这是本教程的一般做法。在产品代码中,我们应该做得更完善些。


 5. 与主机交谈

这里我们实现 getFile() 方法,它将告诉服务器我们想要什么文件并在服务器传回其内容时接收该内容。

public String getFile(String fileNameToGet) {
StringBuffer fileLines = new StringBuffer();

try {
socketWriter.println(fileNameToGet);
socketWriter.flush();

String line = null;
while ((line = socketReader.readLine()) != null)
fileLines.append(line + "\n");
} catch (IOException e) {
System.out.println("Error reading from file: " + fileNameToGet);
}

return fileLines.toString();
}


 

getFile() 方法的调用要求一个有效的文件路径String。它首先创建名为fileLinesStringBufferfileLines 用于存储我们读自服务器上的文件的每一行。

StringBuffer fileLines = new StringBuffer();


 

try{}catch{} 块中,我们用PrintWriter 把请求发送到主机,PrintWriter 是我们在创建连接期间建立的。

socketWriter.println(fileNameToGet);
socketWriter.flush();


 

请注意这里我们是 flush()PrintWriter,而不是关闭它。这迫使数据被发送到服务器而不关闭Socket

一旦我们已经写到 Socket,我们就希望有一些响应。我们不得不在SocketInputStream 上等待它,我们通过在while 循环中调用BufferedReader 上的readLine() 来达到这个目的。我们把每一个返回行附加到fileLinesStringBuffer(带有一个换行符以保护行):

String line = null;
while ((line = socketReader.readLine()) != null)
fileLines.append(line + "\n");


 6. 断开连接

这里我们实现 tearDownConnection() 方法,它将在我们使用完毕连接后负责“清除”:

public void tearDownConnection() {
try {
socketWriter.close();
socketReader.close();
} catch (IOException e) {
System.out.println("Error tearing down socket connection: " + e);
}
}

tearDownConnection() 方法只是分别关闭我们在SocketInputStreamOutputStream 上创建的BufferedReaderPrintWriter。这样做会关闭我们从Socket 获取的底层流,所以我们必须捕捉可能的IOException

 7. 总结一下客户机

我们的类研究完了。在我们继续往前讨论服务器端的情况之前,让我们回顾一下创建和使用 Socket 的步骤:

  1. 用您想连接的机器的 IP 地址和端口实例化 Socket(如有问题则抛出 Exception)。

  2. 获取 Socket 上的流以进行读写。

  3. 把流包装进 BufferedReader/PrintWriter 的实例,如果这样做能使事情更简单的话。

  4. Socket 进行读写。

  5. 关闭打开的流。

   附:RemoteFileClient 的代码清单

import java.io.*;
import java.net.*;

public class RemoteFileClient {
protected BufferedReader socketReader;
protected PrintWriter socketWriter;
protected String hostIp;
protected int hostPort;

public RemoteFileClient(String aHostIp, int aHostPort) {
hostIp = aHostIp;
hostPort = aHostPort;
}
public String getFile(String fileNameToGet) {
StringBuffer fileLines = new StringBuffer();

try {
socketWriter.println(fileNameToGet);
socketWriter.flush();

String line = null;
while ((line = socketReader.readLine()) != null)
fileLines.append(line + "\n");
} catch (IOException e) {
System.out.println("Error reading from file: " + fileNameToGet);
}

return fileLines.toString();
}
public static void main(String[] args) {
RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1", 3000);
remoteFileClient.setUpConnection();
String fileContents = remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");
remoteFileClient.tearDownConnection();

System.out.println(fileContents);
}
public void setUpConnection() {
try {
Socket client = new Socket(hostIp, hostPort);

socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
socketWriter = new PrintWriter(client.getOutputStream());

} catch (UnknownHostException e) {
System.out.println("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);
} catch (IOException e) {
System.out.println("Error setting up socket connection: " + e);
}
}
public void tearDownConnection() {
try {
socketWriter.close();
socketReader.close();
} catch (IOException e) {
System.out.println("Error tearing down socket connection: " + e);
}
}
}




  8. 创建 RemoteFileServer 类

这里是 RemoteFileServer 类的结构:

import java.io.*;
import java.net.*;

public class RemoteFileServer {
protected int listenPort = 3000;
public static void main(String[] args) {
}
public void acceptConnections() {
}
public void handleConnection(Socket incomingConnection) {
}
}


跟客户机中一样,我们首先导入 java.netjava.io。接着,我们给我们的类一个实例变量以保存端口,我们从该端口侦听进入的连接。缺省情况下,端口是 3000。

我们的类有一个 main() 方法和两个其它方法。稍后我们将探究这些方法的细节。现在您只需知道acceptConnections() 将允许客户机连接到服务器以及handleConnection() 与客户机Socket 交互以将您所请求的文件的内容发送到客户机。

 9. 实现 main()

这里我们实现 main() 方法,它将创建 RemoteFileServer 并告诉它接受连接:

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


 

服务器端的 main() 方法甚至比客户机端的更简单。我们实例化一个新 RemoteFileServer,它将在缺省侦听端口上侦听进入的连接请求。然后我们调用acceptConnections() 来告诉该 server 进行侦听。

 10. 接受连接

这里我们实现 acceptConnections() 方法,它将创建一个 ServerSocket 并等待连接请求:

public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort);
Socket incomingConnection = null;
while (true) {
incomingConnection = server.accept();
handleConnection(incomingConnection);
}
} catch (BindException e) {
System.out.println("Unable to bind to port " + listenPort);
} catch (IOException e) {
System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);
}
}


 

acceptConnections() 用欲侦听的端口号来创建ServerSocket。然后我们通过调用该ServerSocketaccept() 来告诉它开始侦听。accept() 方法将造成阻塞直到来了一个连接请求。此时,accept() 返回一个新的 Socket,这个Socket 绑定到服务器上一个随机指定的端口,返回的Socket 被传递给handleConnection()。请注意我们在一个无限循环中处理对连接的接受。这里不支持任何关机。

无论何时如果您创建了一个无法绑定到指定端口(可能是因为别的什么控制了该端口)的 ServerSocket,Java 代码都将抛出一个错误。所以这里我们必须捕捉可能的BindException。就跟在客户机端上时一样,我们必须捕捉IOException,当我们试图在ServerSocket 上接受连接时,它就会被抛出。请注意,您可以通过用毫秒数调用setSoTimeout() 来为accept() 调用设置超时,以避免实际长时间的等待。调用setSoTimeout() 将使accept() 经过指定占用时间后抛出IOException


 11. 处理连接

这里我们实现 handleConnection() 方法,它将用连接的流来接收输入和写输出:

 

public void handleConnection(Socket incomingConnection) {
try {
OutputStream outputToSocket = incomingConnection.getOutputStream();
InputStream inputFromSocket = incomingConnection.getInputStream();

BufferedReader streamReader =
new BufferedReader(new InputStreamReader(inputFromSocket));

FileReader fileReader = new FileReader(new File(streamReader.readLine()));

BufferedReader bufferedFileReader = new BufferedReader(fileReader);
PrintWriter streamWriter =
new PrintWriter(incomingConnection.getOutputStream());
String line = null;
while ((line = bufferedFileReader.readLine()) != null) {
streamWriter.println(line);
}

fileReader.close();
streamWriter.close();
streamReader.close();
} catch (Exception e) {
System.out.println("Error handling a client: " + e);
}
}


 

跟在客户机中一样,我们用 getOutputStream()getInputStream() 来获取与我们刚创建的Socket 相关联的流。跟在客户机端一样,我们把InputStream 包装进BufferedReader,把OutputStream 包装进PrintWriter。在服务器端上,我们需要添加一些代码,用来读取目标文件和把内容逐行发送到客户机。这里是重要的代码:

      FileReader fileReader = new FileReader(new File(streamReader.readLine()));
BufferedReader bufferedFileReader = new BufferedReader(fileReader);
String line = null;
while ((line = bufferedFileReader.readLine()) != null) {
streamWriter.println(line);
}


 

这些代码值得详细解释。让我们一点一点来看:

FileReader fileReader = new FileReader(new File(streamReader.readLine()));


 

首先,我们使用 SocketInputStreamBufferedReader。我们应该获取一条有效的文件路径,所以我们用该路径名构造一个新File。我们创建一个新FileReader 来处理读文件的操作。

BufferedReader bufferedFileReader = new BufferedReader(fileReader);


 

这里我们把 FileReader 包装进BufferedReader 以使我们能够逐行地读该文件。

接着,我们调用 BufferedReaderreadLine()。这个调用将造成阻塞直到有字节到来。我们获取一些字节之后就把它们放到本地的line 变量中,然后再写出到客户机上。完成读写操作之后,我们就关闭打开的流。

请注意我们在完成从 Socket 的读操作之后关闭streamWriterstreamReader。您或许会问我们为什么不在读取文件名之后立刻关闭streamReader。原因是当您这样做时,您的客户机将不会获取任何数据。如果您在关闭streamWriter 之前关闭streamReader,则您可以往Socket 写任何东西,但却没有任何数据能通过通道(通道被关闭了)。


 12. 总结一下服务器

在我们接着讨论另一个更实际的示例之前,让我们回顾一下创建和使用 ServerSocket 的步骤:

  1. 用一个您想让它侦听传入客户机连接的端口来实例化一个 ServerSocket(如有问题则抛出 Exception)。

  2. 调用 ServerSocketaccept() 以在等待连接期间造成阻塞。

  3. 获取位于该底层 Socket 的流以进行读写操作。

  4. 按使事情简单化的原则包装流。

  5. Socket 进行读写。

  6. 关闭打开的流(并请记住,永远不要在关闭 Writer 之前关闭 Reader)。

  附: RemoteFileServer 的完整的代码清单

import java.io.*;
import java.net.*;

public class RemoteFileServer {
int listenPort;
public RemoteFileServer(int aListenPort) {
listenPort = aListenPort;
}
public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort);
Socket incomingConnection = null;
while (true) {
incomingConnection = server.accept();
handleConnection(incomingConnection);
}
} catch (BindException e) {
System.out.println("Unable to bind to port " + listenPort);
} catch (IOException e) {
System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);
}
}
public void handleConnection(Socket incomingConnection) {
try {
OutputStream outputToSocket = incomingConnection.getOutputStream();
InputStream inputFromSocket = incomingConnection.getInputStream();

BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputFromSocket));

FileReader fileReader = new FileReader(new File(streamReader.readLine()));

BufferedReader bufferedFileReader = new BufferedReader(fileReader);
PrintWriter streamWriter = new PrintWriter(incomingConnection.getOutputStream());
String line = null;
while ((line = bufferedFileReader.readLine()) != null) {
streamWriter.println(line);
}

fileReader.close();
streamWriter.close();
streamReader.close();
} catch (Exception e) {
System.out.println("Error handling a client: " + e);
}
}
public static void main(String[] args) {
RemoteFileServer server = new RemoteFileServer(3000);
server.acceptConnections();
}
}