目录
概要
有关套接字的详细介绍,可以看下《think in java》15.2章节。
1. 套接字基本知识
“套接字”或者“插座”(Socket)也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知的。抽象的基本宗旨是让我们尽可能不必知道那些细节。
在Java 中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个InputStream 以及OutputStream(若使用恰当的转换器,则分别是Reader 和Writer),以便将连接作为一个IO 流对象对待。有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;以及Socket,客户用它初始一次连接。一旦客户(程序)申请建立一个套接字连接,ServerSocket 就会返回(通过accept()方法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字”连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用getInputStream()以及getOutputStream()从每个套接字产生对应的InputStream 和OutputStream 对象。这些数据流必须封装到缓冲区内。
对于Java 库的命名机制,ServerSocket(服务器套接字)的使用无疑是容易产生混淆的又一个例证。大家可能认为ServerSocket 最好叫作“ServerConnector”(服务器连接器),或者其他什么名字,只是不要在其中安插一个“Socket”。也可能以为ServerSocket 和Socket 都应从一些通用的基础类继承。事实上,这两种类确实包含了几个通用的方法,但还不够资格把它们赋给一个通用的基础类。相反,ServerSocket 的主要任务是在那里耐心地等候其他机器同它连接,再返回一个实际的Socket。这正是“ServerSocket”这个命名不恰当的地方,因为它的目标不是真的成为一个Socket,而是在其他人同它连接的时候产生一个Socket 对象。
然而,ServerSocket 确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听进入的连接,然后利用accept()方法返回一个“已建立”套接字(本地和远程端点均已定义)。容易混淆的地方是这两个套接字(侦听和已建立)都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接请求,不能接收实际的数据包。所以尽管ServerSocket 对于编程并无太大的意义,但它确实是“物理性”的。
创建一个ServerSocket 时,只需为其赋予一个端口编号。不必把一个IP 地址分配它,因为它已经在自己代表的那台机器上了。但在创建一个Socket 时,却必须同时赋予IP 地址以及要连接的端口编号(另一方面,从ServerSocket.accept()返回的Socket 已经包含了所有这些信息)。
2. 交互过程
连接建立好后,服务器端和客户端的输入流和输出流就互为彼此,即一端的输出流是另一端的输入流。
3. 一个简单的服务器和客户机程序
这个例子将以最简单的方式运用套接字对服务器和客户机进行操作。服务器的全部工作就是等候建立一个连接,然后用那个连接产生的Socket 创建一个InputStream 以及一个OutputStream。在这之后,它从InputStream 读入的所有东西都会反馈OutputStream,直到接收到行中止(END)为止,最后关闭连接。客户机连接与服务器的连接,然后创建一个OutputStream。文本行通过OutputStream 发送。客户机也会创建一个InputStream,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。服务器与客户机(程序)都使用同样的端口号,而且客户机利用本地主机地址连接位于同一台机器中的服务器(程序),所以不必在一个物理性的网络里完成测试(在某些配置环境中,可能需要同真正的网络建立连接,否则程序不能工作——尽管实际并不通过那个网络通信)。
下面是服务器程序:
1 /** 2 * @Title: JabberServer.java 3 * @Description: TODO 4 * @author :Xingle 5 * @date 2014-7-22 下午12:50:53 6 * @version 7 */ 8 9 package com.xingle_test.socket; 10 11 import java.io.BufferedReader; 12 import java.io.BufferedWriter; 13 import java.io.IOException; 14 import java.io.InputStreamReader; 15 import java.io.OutputStreamWriter; 16 import java.io.PrintWriter; 17 import java.net.ServerSocket; 18 import java.net.Socket; 19 20 /** 21 * 一个简单的服务器和客户机程序 22 * 23 * @ClassName: JabberServer TODO 24 * @author Xingle 25 * @date 2014-7-22 下午12:50:53 26 */ 27 public class JabberServer { 28 29 // Choose a port outside of the range 1-1024: 30 public static final int POST = 8080; 31 32 public static void main(String[] args) throws IOException { 33 ServerSocket s = new ServerSocket(POST); 34 System.out.println("Started :" + s); 35 try { 36 // Blocks until a connection occurs: 37 Socket socket = s.accept(); 38 try { 39 System.out.println("Connection acception:" + socket); 40 BufferedReader in = new BufferedReader(new InputStreamReader( 41 socket.getInputStream())); 42 // Output is automatically flushed by PrintWriter: 43 PrintWriter out = new PrintWriter(new BufferedWriter( 44 (new OutputStreamWriter(socket.getOutputStream()))), 45 true); 46 while (true) { 47 String str = in.readLine(); 48 if (str.equals("END")) 49 break; 50 System.out.println("Echoing:" + str); 51 out.println(str); 52 } 53 // Always close the two sockets... 54 } finally { 55 System.out.println("service closing..."); 56 socket.close(); 57 } 58 } finally { 59 s.close(); 60 } 61 } 62 }
下面是客户程序的源码:
1 /** 2 * @Title: JabberClient.java 3 * @Description: TODO 4 * @author :Xingle 5 * @date 2014-7-22 下午1:53:35 6 * @version 7 */ 8 9 package com.xingle_test.socket; 10 11 import java.io.BufferedReader; 12 import java.io.IOException; 13 import java.io.InputStreamReader; 14 import java.io.OutputStreamWriter; 15 import java.io.PrintWriter; 16 import java.net.InetAddress; 17 import java.net.Socket; 18 19 /** 20 * 21 * @ClassName: JabberClient TODO 22 * @author Xingle 23 * @date 2014-7-22 下午1:53:35 24 */ 25 public class JabberClient { 26 27 public static void main(String[] args) throws IOException { 28 // testing on one machine w/o a network: 29 InetAddress addr = InetAddress.getByName(null); 30 // Alternatively, you can use 31 // the address or name: 32 // InetAddress addr = 33 // InetAddress.getByName("127.0.0.1"); 34 // InetAddress addr = 35 // InetAddress.getByName("localhost"); 36 System.out.println("addr=" + addr); 37 Socket socket = new Socket(addr, JabberServer.POST); 38 // Guard everything in a try-finally to make sure that the socket is closed: 39 try { 40 System.out.println("socket=" + socket); 41 BufferedReader in = new BufferedReader(new InputStreamReader( 42 socket.getInputStream())); 43 // Output is automatically flushed by PrintWriter: 44 PrintWriter out = new PrintWriter(new PrintWriter( 45 new OutputStreamWriter(socket.getOutputStream())), true); 46 for (int i = 0; i < 10; i++) { 47 out.println("howdy:"+i); 48 String str = in.readLine(); 49 System.out.println(str); 50 } 51 out.println("END"); 52 } finally { 53 System.out.println("closing..."); 54 socket.close(); 55 } 56 } 57 58 }
然后先运行服务器端,再运行客户端
客户端的执行结果:
addr=localhost/127.0.0.1
socket=Socket[addr=localhost/127.0.0.1,port=8080,localport=58540]
howdy:0
howdy:1
howdy:2
howdy:3
howdy:4
howdy:5
howdy:6
howdy:7
howdy:8
howdy:9
closing...
服务器端的执行结果:
Started :ServerSocket[addr=0.0.0.0/0.0.0.0,localport=8080]
Connection acception:Socket[addr=/127.0.0.1,port=58540,localport=8080]
Echoing:howdy:0
Echoing:howdy:1
Echoing:howdy:2
Echoing:howdy:3
Echoing:howdy:4
Echoing:howdy:5
Echoing:howdy:6
Echoing:howdy:7
Echoing:howdy:8
Echoing:howdy:9
service closing...
4. 服务多个客户
最基本的方法是在服务器(程序)里创建单个ServerSocket,并调用accept()来等候一个新连接。一旦accept()返回,我们就取得结果获得的Socket,并用它新建一个线程,令其只为那个特定的客户服务。然后再调用accept() ,等候下一次新的连接请求。
对于下面这段服务器代码,可发现它与JabberServer.java 例子非常相似,只是为一个特定的客户提供服务的所有操作都已移入一个独立的线程类中:
服务器代码:
1 package com.xingle_test.socket; 2 3 import java.io.*; 4 import java.net.ServerSocket; 5 import java.net.Socket; 6 7 /** 8 * 服务器端 9 * @ClassName: MultiJabberServer 10 * @author Xingle 11 * @date 2014-7-23 下午9:10:23 12 */ 13 public class MultiJabberServer { 14 static final int PORT = 8080; 15 16 public static void main(String[] args) throws IOException { 17 ServerSocket s = new ServerSocket(PORT); 18 System.out.println("Server Started"); 19 try { 20 while (true) { 21 // Blocks until a connection occurs: 22 Socket socket = s.accept(); 23 try { 24 new ServeOneJabber(socket); 25 } catch (IOException e) { 26 // If it fails, close the socket, 27 // otherwise the thread will close it: 28 socket.close(); 29 } 30 } 31 } finally { 32 s.close(); 33 } 34 } 35 } 36 37 class ServeOneJabber extends Thread { 38 private Socket socket; 39 private BufferedReader in; 40 private PrintWriter out; 41 42 public ServeOneJabber(Socket s) throws IOException { 43 socket = s; 44 in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 45 // Enable auto-flush: 46 out = new PrintWriter(new BufferedWriter(new OutputStreamWriter( 47 socket.getOutputStream())), true); 48 // If any of the above calls throw an 49 // exception, the caller is responsible for 50 // closing the socket. Otherwise the thread 51 // will close it. 52 start(); // Calls run() 53 } 54 55 public void run() { 56 try { 57 while (true) { 58 String str = in.readLine(); 59 if (str.equals("END")) 60 break; 61 System.out.println("Echoing: " + str); 62 out.println(str); 63 } 64 System.out.println("closing..."); 65 } catch (IOException e) { 66 } finally { 67 try { 68 socket.close(); 69 } catch (IOException e) { 70 } 71 } 72 } 73 }
和以前一样,我们创建一个ServerSocket,并调用accept()允许一个新连接的建立。但这一次,accept() 的返回值(一个套接字)将传递给用于ServeOneJabber 的构建器,由它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。
如果ServerSocket 创建失败,则再一次通过main()掷出违例。如果成功,则位于外层的try-finally 代码块可以担保正确的清除。位于内层的try-catch 块只负责防ServeOneJabber 构建器的失败;若构建器成功,则ServeOneJabber 线程会将对应的套接字关掉。
为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许创建的线程的最大数量是由final int maxthreads 决定的。大家会注意到这个值非常关键,因为假如把它设得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。
客户端程序:
1 package com.xingle_test.socket; 2 3 import java.io.*; 4 import java.net.InetAddress; 5 import java.net.Socket; 6 7 /** 8 * 客户端 9 * @ClassName: MultiJabberClient 10 * @author Xingle 11 * @date 2014-7-23 下午9:13:10 12 */ 13 public class MultiJabberClient { 14 static final int MAX_THREADS = 4; 15 16 public static void main(String[] args) throws IOException, 17 InterruptedException { 18 InetAddress addr = InetAddress.getByName(null); 19 while (true) { 20 if (JabberClientThread.threadCount() < MAX_THREADS) 21 new JabberClientThread(addr); 22 Thread.currentThread().sleep(100); 23 } 24 } 25 } 26 27 class JabberClientThread extends Thread { 28 private Socket socket; 29 private BufferedReader in; 30 private PrintWriter out; 31 private static int counter = 0; 32 private int id = counter++; 33 private static int threadcount = 0; 34 35 public static int threadCount() { 36 return threadcount; 37 } 38 39 public JabberClientThread(InetAddress addr) { 40 System.out.println("Making client " + id); 41 threadcount++; 42 try { 43 socket = new Socket(addr, MultiJabberServer.PORT); 44 } catch (IOException e) { 45 // If the creation of the socket fails, 46 // nothing needs to be cleaned up. 47 } 48 try { 49 in = new BufferedReader(new InputStreamReader( 50 socket.getInputStream())); 51 // Enable auto-flush: 52 out = new PrintWriter(new BufferedWriter(new OutputStreamWriter( 53 socket.getOutputStream())), true); 54 start(); 55 } catch (IOException e) { 56 // The socket should be closed on any 57 // failures other than the socket 58 // constructor: 59 try { 60 socket.close(); 61 } catch (IOException e2) { 62 } 63 } 64 // Otherwise the socket will be closed by 65 // the run() method of the thread. 66 } 67 68 public void run() { 69 try { 70 for (int i = 0; i < 5; i++) { 71 out.println("Client " + id + ": " + i); 72 String str = in.readLine(); 73 System.out.println(str); 74 } 75 out.println("END"); 76 } catch (IOException e) { 77 } finally { 78 // Always close it: 79 try { 80 socket.close(); 81 } catch (IOException e) { 82 } 83 threadcount--; // Ending this thread 84 } 85 } 86 }
首先执行服务器端程序,再执行客户端程序,先看下客户端结果:
Making client 0
Client 0: 0
Client 0: 1
Client 0: 2
Client 0: 3
Client 0: 4
Making client 1
Client 1: 0
Client 1: 1
Client 1: 2
Client 1: 3
Client 1: 4
Making client 2
Client 2: 0
Client 2: 1
Client 2: 2
Client 2: 3
Client 2: 4
Making client 3
Client 3: 0
Client 3: 1
Client 3: 2
Client 3: 3
Client 3: 4
Making client 4
Client 4: 0
Client 4: 1
Client 4: 2
Client 4: 3
Client 4: 4
Making client 5
Client 5: 0
Client 5: 1
Client 5: 2
Client 5: 3
Client 5: 4
Making client 6
Client 6: 0
Client 6: 1
Client 6: 2
Client 6: 3
Client 6: 4
Making client 7
Client 7: 0
Client 7: 1
.
.
.
(截取一部分)
再看下服务器端console:
Server Started
Echoing: Client 0: 0
Echoing: Client 0: 1
Echoing: Client 0: 2
Echoing: Client 0: 3
Echoing: Client 0: 4
closing...
Echoing: Client 1: 0
Echoing: Client 1: 1
Echoing: Client 1: 2
Echoing: Client 1: 3
Echoing: Client 1: 4
closing...
Echoing: Client 2: 0
Echoing: Client 2: 1
Echoing: Client 2: 2
Echoing: Client 2: 3
Echoing: Client 2: 4
closing...
Echoing: Client 3: 0
Echoing: Client 3: 1
Echoing: Client 3: 2
Echoing: Client 3: 3
Echoing: Client 3: 4
closing...
Echoing: Client 4: 0
Echoing: Client 4: 1
Echoing: Client 4: 2
Echoing: Client 4: 3
Echoing: Client 4: 4
closing...
Echoing: Client 5: 0
Echoing: Client 5: 1
Echoing: Client 5: 2
Echoing: Client 5: 3
Echoing: Client 5: 4
closing...
Echoing: Client 6: 0
Echoing: Client 6: 1
Echoing: Client 6: 2
Echoing: Client 6: 3
Echoing: Client 6: 4
closing...
Echoing: Client 7: 0
Echoing: Client 7: 1
Echoing: Client 7: 2
.
.
.
(截取一部分)
JabberClientThread 构建器获取一个InetAddress,并用它打开一个套接字。大家可能已看出了这样的一个套路:Socket 肯定用于创建某种Reader 以及/或者Writer(或者InputStream 和/或OutputStream)对象,这是运用Socket 的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重复的代码编写工作)。同样地,start()执行线程的初始化,并调用run()。在这里,消息发送给服务器,而来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接字创建好以后,但在构建器完成之前,假若构建器失败,套接字会被清除。否则,为套接字调用close()的责任便落到了run()方法的头上。
threadcount 跟踪计算目前存在的JabberClientThread 对象的数量。它将作为构建器的一部分增值,并在run()退出时减值(run()退出意味着线程中止)。在MultiJabberClient.main()中,大家可以看到线程的数量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS,看看对于你使用的系统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。