1.ServerSocket;
ServerSocket有以下3 个选项。
l SO_TIMEOUT:表示等待客户连接的超时时间。
l SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。
l SO_RCVBUF:表示接收数据的缓冲区的大小。
SO_TIMEOUT选项
l 设置该选项:public void setSoTimeout(int timeout) throws SocketException
l 读取该选项:public int getSoTimeout () throws IOException
SO_TIMEOUT 表示ServerSocket 的accept()方法等待客户连接的超时时间,以毫
秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的
默认值。
当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就
会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那
么当服务器等待的时间超过了超时时间,就会抛出SocketTimeoutException,它是
InterruptedException的子类。
example:
import java.io.*; import java.net.*; public class TimeoutTester{ public static void main(String args[])throws IOException{ ServerSocket serverSocket=new ServerSocket(8000); serverSocket.setSoTimeout(6000); //等待客户连接的时间不超过6 秒 Socket socket=serverSocket.accept(); socket.close(); System.out.println("服务器关闭"); } }
运行6秒后会抛出异常:
C:\chapter03\classes>java TimeoutTester
Exception in thread "main" java.net.SocketTimeoutException: Accept timed out
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.PlainSocketImpl.accept(Unknown Source)
at java.net.ServerSocket.implAccept(Unknown Source)
at java.net.ServerSocket.accept(Unknown Source)
at TimeoutTester.main(TimeoutTester.java:8)
如果把程序中的“serverSocket.setSoTimeout(6000)”注释掉,那么serverSocket.
accept()方法永远不会超时,它会一直等待下去,直到接收到了客户的连接,才会从
accept()方法返回。
2.ServerSocket 的构造方法
l ServerSocket()throws IOException
l ServerSocket(int port) throws IOException
l ServerSocket(int port, int backlog) throws IOException
l ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在以上构造方法中,参数port 指定服务器要绑定的端口(服务器要监听的端口),
参数backlog指定客户连接请求队列的长度,参数bindAddr指定服务器要绑定的IP地址。
import java.net.*; public class Client { public static void main(String args[])throws Exception{ final int length=100; String host="localhost"; int port=8000; Socket[] sockets=new Socket[length]; for(int i=0;i<length;i++){ //试图建立100 次连接 sockets[i]=new Socket(host, port); System.out.println("第"+(i+1)+"次连接成功"); } Thread.sleep(3000); for(int i=0;i<length;i++){ sockets[i].close(); //断开连接 } } } import java.io.*; import java.net.*; public class Server { private int port=8000; private ServerSocket serverSocket; public Server() throws IOException { serverSocket = new ServerSocket(port,3); //连接请求队列的长度为3 System.out.println("服务器启动"); } public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); //从连接请求队列中取出一个连接 System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); }catch (IOException e) { e.printStackTrace(); }finally { try{ if(socket!=null)socket.close(); }catch (IOException e) {e.printStackTrace();} } } } public static void main(String args[])throws Exception { Server server=new Server(); Thread.sleep(60000*10); //睡眠10 分钟 //server.service(); } }
Client 试图与Server 进行100 次连接。在Server 类中,把连接请求队列的长度设
为3。这意味着当队列中有了3 个连接请求时,如果Client 再请求连接,就会被Server
拒绝
结是如下:
第1 次连接成功
第2 次连接成功
第3 次连接成功
Exception in thread "main" java.net.ConnectException: Connection refused: connect
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.PlainSocketImpl.doConnect(Unknown Source)
at java.net.PlainSocketImpl.connectToAddress(Unknown Source)
at java.net.PlainSocketImpl.connect(Unknown Source)
at java.net.SocksSocketImpl.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at java.net.Socket.connect(Unknown Source)
at java.net.Socket.<init>(Unknown Source)
at java.net.Socket.<init>(Unknown Source)
at Client.main(Client.java:10)
从以上打印结果可以看出,Client 与Server 在成功地建立了3 个连接后,就无法
再创建其余的连接了,因为服务器的队列已经满了。
如果是改为
public static void main(String args[])throws Exception {
Server server=new Server();
//Thread.sleep(60000*10); //睡眠10 分钟
server.service();
}
2.1 设定绑定的IP地址
如果主机只有一个IP 地址,那么默认情况下,服务器程序就与该IP 地址绑定。
ServerSocket的第4 个构造方法ServerSocket(int port, int backlog, InetAddress bindAddr)
有一个bindAddr 参数,它显式指定服务器要绑定的IP地址,该构造方法适用于具有多
个IP地址的主机。假定一个主机有两个网卡,一个网卡用于连接到Internet, IP地址
为222.67.5.94,还有一个网卡用于连接到本地局域网,IP 地址为192.168.3.4。如果服
务器仅仅被本地局域网中的客户访问,那么可以按如下方式创建ServerSocket:
ServerSocket serverSocket=new ServerSocket(8000,10,InetAddress.getByName ("192.168.3.4"));
2.2默认构造方法的作用
ServerSocket有一个不带参数的默认构造方法。通过该方法创建的ServerSocket不
与任何端口绑定,接下来还需要通过bind()方法与特定端口绑定。
这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置
ServerSocket的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。
在以下代码中,先把ServerSocket 的SO_REUSEADDR 选项设为true,然后再把
它与8000 端口绑定:
ServerSocket serverSocket=new ServerSocket();
serverSocket.setReuseAddress(true); //设置ServerSocket 的选项
serverSocket.bind(new InetSocketAddress(8000)); //与8000 端口绑定
如果把以上程序代码改为:
ServerSocket serverSocket=new ServerSocket(8000);
serverSocket.setReuseAddress(true); //设置ServerSocket 的选项
那么serverSocket.setReuseAddress(true)方法就不起任何作用了,因为SO_
REUSEADDR选项必须在服务器绑定端口之前设置才有效。
打印结果如下:
第1 次连接成功
第2 次连接成功
第3 次连接成功
…
第100 次连接成功
3. setResuseAddress(boolean b) 的用法
这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数
据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket
同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,
允许重用端口,而在某些操作系统中不允许重用端口。
当ServerSocket 关闭时,如果网络上还有发送到这个ServerSocket 的数据,这个
ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送
过来的延迟数据,然后再释放端口。
许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会
被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占
用,使得服务器程序无法绑定到该端口,服务器启动失败,并抛出BindException:
Exception in thread "main" java.net.BindException: Address already in use: JVM_Bind
为了确保一个进程关闭了ServerSocket 后,即使操作系统还没释放端口,同一个
主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket 的setResuse-
Address(true)方法:
if(!serverSocket.getResuseAddress())serverSocket.setResuseAddress(true);
值得注意的是,serverSocket.setResuseAddress(true)方法必须在ServerSocket还没有
绑定到一个本地端口之前调用,否则执行serverSocket.setResuseAddress(true)方法无效。
此外,两个共用同一个端口的进程必须都调用serverSocket.setResuseAddress(true)方法,
才能使得一个进程关闭ServerSocket后,另一个进程的ServerSocket还能够立刻重用相
同端口。
4.SO_RCVBUF选项
SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般
说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓
冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信
(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。
ServerSocket serverSocket=new ServerSocket();
int size=serverSocket.getReceiveBufferSize();
if(size<131072) serverSocket.setReceiveBufferSize(131072); //把缓冲区的大小设为128K
serverSocket.bind(new InetSocketAddress(8000)); //与8 000 端口绑定
5.创建线程池
l 除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。每个线程本
身都会占用一定的内存(每个线程需要大约1M 内存),如果同时有大量客户
连接服务器,就必须创建大量工作线程,它们消耗了大量内存,可能会导致
系统的内存空间不足。
l 如果线程数目固定,并且每个线程都有很长的生命周期,那么线程切换也是
相对固定的。不同操作系统有不同的切换周期,一般在20 毫秒左右。这里所
说的线程切换是指在Java 虚拟机,以及底层操作系统的调度下,线程之间转
让CPU的使用权。如果频繁创建和销毁线程,那么将导致频繁地切换线程,
因为一个线程被销毁后,必然要把CPU转让给另一个已经就绪的线程,使该
线程获得运行机会。在这种情况下,线程之间的切换不再遵循系统的固定切
换周期,切换线程的开销甚至比创建及销毁线程的开销还大。
采用线程池:预先创建了一些工作线程,它们不断从工作队列中取出任务,然后执行该任务。当工
作线程执行完一个任务时,就会继续执行工作队列中的下一个任务。线程池具有以下
优点:
l 减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多
个任务。
l 可以根据系统的承载能力,方便地调整线程池中线程的数目,防止因为消耗
过量系统资源而导致系统崩溃。
package multithread2; import java.util.LinkedList; public class ThreadPool extends ThreadGroup { private boolean isClosed=false; //线程池是否关闭 private LinkedList<Runnable> workQueue; //表示工作队列 private static int threadPoolID; //表示线程池ID private int threadID; //表示工作线程ID public ThreadPool(int poolSize) { //poolSize 指定线程池中的工作线程数目 super("ThreadPool-" + (threadPoolID++)); setDaemon(true); workQueue = new LinkedList<Runnable>(); //创建工作队列 for (int i=0; i<poolSize; i++) new WorkThread().start(); //创建并启动工作线程 } /** 向工作队列中加入一个新任务,由工作线程去执行该任务 */ public synchronized void execute(Runnable task) { if (isClosed) { //线程池被关则抛出IllegalStateException异常 throw new IllegalStateException(); } if (task != null) { workQueue.add(task); notify(); //唤醒正在getTask()方法中等待任务的工作线程 } } /** 从工作队列中取出一个任务,工作线程会调用此方法 */ protected synchronized Runnable getTask()throws InterruptedException{ while (workQueue.size() == 0) { if (isClosed) return null; wait(); //如果工作队列中没有任务,就等待任务 } return workQueue.removeFirst(); } /** 关闭线程池 */ public synchronized void close() { if (!isClosed) { isClosed = true; workQueue.clear(); //清空工作队列 interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup类 } } /** 等待工作线程把所有任务执行完 */ public void join() { synchronized (this) { isClosed = true; notifyAll(); //唤醒还在getTask()方法中等待任务的工作线程 } Thread[] threads = new Thread[activeCount()]; //enumerate()方法继承自ThreadGroup类,获得线程组中当前所有活着的工作线程 int count = enumerate(threads); for (int i=0; i<count; i++) { //等待所有工作线程运行结束 try { threads[i].join(); //等待工作线程运行结束 }catch(InterruptedException ex) { } } } /** 内部类:工作线程 */ private class WorkThread extends Thread { public WorkThread() { //加入到当前ThreadPool 线程组中 super(ThreadPool.this,"WorkThread-" + (threadID++)); } public void run() { while (!isInterrupted()) { //isInterrupted()方法继承自Thread 类,判断线程是否被中断 Runnable task = null; try { //取出任务 task = getTask(); }catch (InterruptedException ex){} // 如果getTask()返回null 或者线程执行getTask()时被中断,则结束此线程 if (task == null) return; try { //运行任务,异常在catch代码块中捕获 task.run(); } catch (Throwable t) { t.printStackTrace(); } } //#while } //#run() } //#WorkThread 类 }
在ThreadPool类中定义了一个LinkedList类型的workQueue成员变量,它表示工
作队列,用来存放线程池要执行的任务,每个任务都是Runnable实例。ThreadPool 类
的客户程序(利用ThreadPool 来执行任务的程序)只要调用ThreadPool 类的execute
(Runnable task)方法,就能向线程池提交任务。在ThreadPool类的execute()方法中,先
判断线程池是否已经关闭。如果线程池已经关闭,就不再接收任务,否则就把任务加
入到工作队列中,并且唤醒正在等待任务的工作线程。
在ThreadPool 类的构造方法中,会创建并启动若干工作线程,工作线程的数目由
构造方法的参数poolSize决定。WorkThread类表示工作线程,它是ThreadPool类的内
部类。工作线程从工作队列中取出一个任务,接着执行该任务,然后再从工作队列中
取出下一个任务并执行它,如此反复。
工作线程从工作队列中取任务的操作是由ThreadPool 类的getTask()方法实现的,
它的处理逻辑如下:
l 如果队列为空并且线程池已关闭,那就返回null,表示已经没有任务可以执
行了;
l 如果队列为空并且线程池没有关闭,那就在此等待,直到其他线程将其唤醒
或者中断;
l 如果队列中有任务,就取出第一个任务并将其返回。
线程池的join()和close()方法都可用来关闭线程池。join()方法确保在关闭线程池之
前,工作线程把队列中的所有任务都执行完。而close()方法则立即清空队列,并且中
断所有的工作线程。
ThreadPool 类是ThreadGroup类的子类。ThreadGroup 类表示线程组,它提供了一
些管理线程组中线程的方法。例如,interrupt()方法相当于调用线程组中所有活着的线
程的interrupt()方法。线程池中的所有工作线程都加入到当前ThreadPool 对象表示的线
程组中。ThreadPool类在close()方法中调用了interrupt()方法:
/** 关闭线程池 */
public synchronized void close() {
if (!isClosed) {
isClosed = true;
workQueue.clear(); //清空工作队列
interrupt(); //中断所有的工作线程,该方法继承自ThreadGroup类
}
}
以上interrupt()方法用于中断所有的工作线程。interrupt()方法会对工作线程造成以
下影响:
l 如果此时一个工作线程正在ThreadPool 的getTask()方法中因为执行wait()方
法而阻塞,则会抛出InterruptedException;
l 如果此时一个工作线程正在执行一个任务,并且这个任务不会被阻塞,那么
这个工作线程会正常执行完任务,但是在执行下一轮while (!isInterrupted())
{…}循环时,由于isInterrupted()方法返回true,因此退出while循环。
如例程3-7所示,ThreadPoolTester 类用于测试ThreadPool的用法。