URL和URLConnection类提供了相对高层次的访问网络资源的途径,但是有时程序需要低层次的网络通信,比如当编写客户端—服务器应用程序时。在一个客户端—服务器应用程序中,服务器提供了诸如处理数据查询或者发送当前股票价格的服务。客户端使用服务器提供的服务或者显示数据查询结果或者为投资者提供股票购买建议。发生在客户端和服务器之间的通信必须是可靠的,这样不会出现数据丢失,并且数据必须以服务器发送的顺序到达客户端。
TCP提供了可靠的,端到端的通信通道,客户端—服务器应用程序使用该通道彼此通信。为了在TCP上通信,客户端和服务器程序之间需要建立一个连接,每个程序绑定一个Socket到该连接的己方一端。为了通信,客户端和服务器各自从绑定到该连接的Socket读和写。
Socket简介套接字(Socket)是运行在网络上的两个程序之间 双向通信链接的一个端点。套接字绑定到一个端口号,这样TCP层可以识别数据要被发送到的应用程序。
通常服务器应用程序运行在特定的计算机上并且拥有绑定到特定端口的套接字。服务器只是等待,监听客户端连接请求的套接字,在客户端一端,客户端知道服务器运行的主机名称和服务器正在监听的端口号。为了进行连接请求,客户端尝试在指定的服务器主机和端口上与服务器会和,客户端也需要向服务器标示自己,这样客户端绑定到一个本地端口,该端口将在连接中使用,通常该端口由系统分配。该过程可由下图概括:
如果一切进行顺利,服务器接受该连接。一旦接受,服务器使用一个新的套接字连接到相同的本地端口,并且将远程端点设置为客户端的地址和端口号。之所以需要一个新的套接字是因为这样服务器可以继续监听原来用于连接请求的套接字。一旦连接被接受,在客户端上一个套接字被成功创建,然后客户端可以使用该套接字与服务器通信。该过程可由下图概括:
上文提到套接字是一个端点,更直接地描述一个端点就是一个IP地址和端口号的组合。每个TCP连接可以被它的连个端点唯一地标识,这样可以在自己的主机和服务器之间拥有多个连接。
包java.net提供了Socket类,该类实现了两个Java程序之间双向连接的一端。Socket类是平台独立的实现,隐藏了任何特殊系统的细节。通过使用Socket类而不是依赖本地代码,Java程序可以平台无关的方式在网络上通信。另外java.net包提供了ServerSocket类,服务器可以使用该类监听和接受客户端的连接。
如果想要连接Web,URL类及相关URLConnection类可能比Socket类更加合适,实际上URL类是相对高层次的连接Web的类,并且使用Socket作为底层实现的一部分。
Socke编程实践在简单介绍了一些Socket的知识后,现在通过一个简单的例子看看如何操作Socket。下面先给出源代码,然后再进行分析。首先是客户端的代码:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; public class EchoClient { public static void main(String[] args) throws IOException { Socket echoSocket = null; PrintWriter out = null; BufferedReader in = null; try { //创建连接到服务器的Socket,需要指定主机名和端口号 echoSocket = new Socket("localhost", 9999); System.out.println("The server port is :" + echoSocket.getPort()); System.out.println("The client port is :" + echoSocket.getLocalPort()); //打开该Socket的输出流,使用该输出流写数据 out = new PrintWriter(echoSocket.getOutputStream(), true); //打开该Socket的输入流,使用该流读取数据 in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream())); } catch (UnknownHostException e) { System.err.println("Don't know about host: localhost."); System.exit(1); } catch (IOException e) { System.err.println("Couldn't get I/O for the connection to: localhost."); System.exit(1); } BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); String userInput; while ((userInput = stdIn.readLine()) != null) { out.println(userInput); System.out.println("echo: " + in.readLine()); } //在关闭Socket之前关闭与之关联的输入输出流 out.close(); in.close(); stdIn.close(); echoSocket.close(); } }
接下来是服务器代码:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; public class EchoServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; try { //创建服务器Socket serverSocket = new ServerSocket(9999); } catch (IOException e) { System.err.println("Could not listen on port: 9999."); System.exit(1); } Socket clientSocket = null; try { //监听到此套接字的请求,并创建新的套接字,使用新的套接字与客户端通信 clientSocket = serverSocket.accept(); System.out.println("The server port is :" + clientSocket.getLocalPort()); System.out.println("The client port is :" + clientSocket.getPort()); } catch (IOException e) { System.err.println("Accept failed."); System.exit(1); } PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String inputLine; //out.println(outputLine); while ((inputLine = in.readLine()) != null) { out.println(inputLine); if (inputLine.equals("Bye")) break; } out.close(); in.close(); clientSocket.close(); serverSocket.close(); } }
要演示该程序的执行效果,首先启动服务器程序,然后再启动客户端。在客户端和服务器的控制台首先会输出连接的服务器端口号和客户端端口号,然后在客户段输入,服务器收到客户端的输入后会原样的返回给客户端。
客户端的输出为:
The server port is :9999
The client port is :54555
Hello Java
echo: Hello Java
This is my first socket program
echo: This is my first socket program
服务器的输出为:
The server port is :9999
The client port is :54555
前面曾提到过,一旦接受,服务器使用一个新的套接字连接到相同的本地端口,并且将远程端点设置为客户端的地址和端口号。下面的代码为服务器的输出代码,从该代码及服务器的输出可以发现,新的套接字clientSocket使用了相同的本地端口(9999),远程端口(54555)由系统自动分配。
clientSocket = serverSocket.accept(); System.out.println("Theserver port is :" + clientSocket.getLocalPort()); System.out.println("The client port is :" + clientSocket.getPort());
总结Socket编程如下:
1. 打开一个Socket
2. 打开Socket的输入输出流
3. 从输入流中读数据,向输出流中写数据
4. 关闭输入输出流
5. 关闭Socket