主要的几个构造方法
ServerSocket() throws IOException
ServerSocket(int port) throws IOException
ServerSocket(int port, int backlog) throws IOException
ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在以上构造方法中,参数port制定服务器要绑定的端口,参数backlog指定客户连接请求队列的长度,参数bindAddr指定服务器要绑定的IP地址。
绑定端口
可能出现BindException,可能的原因,端口已经被其他服务器进程占用;在某些操作系统中,如果没有以超级用户来运行服务器程序,那么操作系统不允许服务器绑定到1~1023之间的端口(一般也不要去触碰这些端口);如果把参数port设为0,标示由操作系统来为服务器分配一个任意可用的端口。
请求队列长度
管理客户请求的任务是由操作系统来完成的,操作系统把这些连接请求存在一个先进先出的队列中。当到达最大长度时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。
对于客户进程,被拒绝后抛出ConnectionException。
设定IP地址
多个网卡要确定用哪个网络的情况下才会用bindAddr。
默认构造方法和客户端的Scoket()一个作用,为了设置一些条件后再进行bind,比如说SO_REUSEADDR.
接收和关闭与客户的连接
如果队列中没有连接请求,accept()方法就会一直等待,这是个阻塞的过程。当有连接请求后,accept()方法取出一个Socket对象,接下来,服务器从Socket对象中获得输入流和输出流,和客户端交换数据。如果服务器正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端就会抛出一个IOException的子类SocketException异常:
java.net.SocketException:Connection reset by peer
关闭Serversocket
在某些情况下,如果希望及时释放服务器的端口,需要显示的调用close(),其实作为服务端,需要关闭的情况应该不会太多。
获取ServerSocket的信息
ServerSocket的两个get方法获取服务器绑定的IP地址和端口
getInetAddress();
getLocalPort();
构造ServerSocket时,如果把端口设置为0,那么由操作系统为服务器分配一个端口(匿名端口),多数服务器会监听一个固定端口,这样才便于客户程序访问服务器。匿名端口一般适用于服务器与客户之间的临时通信,通信结束,就断开连接,并且ServerSocket占用的临时端口也被释放了。
FTP就使用了匿名端口
FTP使用两个并行的TCP连接:一个是控制连接,一个是数据连接。控制连接用于在客户和服务器之间发送控制信息,如用户名和口令、改变远程目录的命令或上传和下载文件的命令。数据连接用于传送文件。TCP服务器在21端口上监听控制连接,如果有客户要求上传或下载文件,就另外建立一个数据连接,通过它来传送文件。数据连接的建立有两种方式。
如下图,TCP服务器在20端口上监听数据连接,TCP客户主动请求建立与该端口的连接。
首先由TCP客户创建一个监听匿名端口的ServerSocket,再把这个ServerSocket监听的端口号(调用ServerSocket的getLocalPort()方法就能得到端口号)发送给TCP服务器,然后由TCP服务器主动请求建立与客户端的连接。
以上第二种方式就使用了匿名端口,并且是在客户端使用的,用于和服务器建立临时的数据连接。在实际应用中,在服务器端也可以使用匿名端口。
ServerSocket选项
SO_TIMEOUT:表示等待客户连接的超时时间。
SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。
SO_RCVBUF:表示接收数据的缓冲区的大小。
SO_TIMEOUT选项
设置该选项:public void setSoTimeout(int timeout) throws SocketException
读取该选项:public int getSoTimeout () throws IOException
SO_TIMEOUT表示ServerSocket的accept()方法等待客户连接的超时时间,以毫秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。
当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那么当服务器等待的时间超过了超时时间,就会抛出SocketTimeoutException,它是InterruptedException的子类。
SO_REUSEADDR选项
设置该选项:public void setResuseAddress(boolean on) throws SocketException
读取该选项:public boolean getResuseAddress() throws SocketException
这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口。
当ServerSocket关闭时,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,服务器启动失败,并抛出BindException
值得注意的是,serverSocket.setResuseAddress(true)方法必须在ServerSocket还没有绑定到一个本地端口之前调用,否则执行serverSocket.setResuseAddress(true)方法无效。此外,两个共用同一个端口的进程必须都调用serverSocket.setResuseAddress(true)方法,才能使得一个进程关闭ServerSocket后,另一个进程的ServerSocket还能够立刻重用相同端口。
SO_RCVBUF选项
设置该选项:public void setReceiveBufferSize(int size) throws SocketException
读取该选项:public int getReceiveBufferSize() throws SocketException
SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。
无论在ServerSocket绑定到特定端口之前或之后,调用setReceiveBufferSize()方法都有效。例外情况下是如果要设置大于64K的缓冲区,则必须在ServerSocket绑定到特定端口之前进行设置才有效。
执行serverSocket.setReceiveBufferSize()方法,相当于对所有由serverSocket.accept()方法返回的Socket设置接收数据的缓冲区的大小。
创建线程池
为每个客户分配一个新的工作线程,当工作线程与客户通信结束,这个线程就被销毁,这种实现方式有以下不足之处:
1,服务器创建和销毁工作线程的开销(包括所花费的时间和系统资源)很大。
2,除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。每个线程本身都会占用一定的内存(每个线程需要大约1MB内存)。
3,如果线程数目固定,并且每个线程都有很长的生命周期,那么线程切换也是相对固定的。不同的操作系统有不同的切换周期,一般在20ms左右。这里所说的线程切换是指在Java虚拟机,以及底层操作系统的调度下,线程之间转让CPU的使用权。如果频繁穿件和销毁线程,那么将导致频繁地切换线程,因为一个线程被销毁后,必然要把CPU转让给另一个已经就绪的线程,使该线程获得运行机会。在这种情况下,线程之间的切换不在遵循系统的固定切换周期,切换线程的开销甚至比创建线程的开销还大。
线程池为线程生命周期开销问题和系统资源不足的问题提供了解决方案。
减少创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多个任务。
可以根据系统的承载能力,方便的调整线程池中线程的数目,防止因为消耗过量系统资源而系统崩溃。
ThreadPool类提供了线程池的一种实现方案。
引用http://hi.baidu.com/dotfire/blog/item/ad5eef60d6235eda8cb10d64.html