Java网络编程精解笔记3:ServerSocket详解
ServerSocket
用法详解
1.C/S模式中,Server需要创建特定端口的ServerSocket.->其负责接收client连接请求.
2. 线程池->包括一个工作队列和若干工作线程->工作线程不断的从工作队列中取出任务并执行 .-->java.util.concurrent->线程池
3.server可利用多线程来处理与多个客户的通信任务.
1. 构造ServerSocket
1.重载形式
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地址.
2.除了第一个构造方法,其他方法都会与特定端口绑定.
a.如ServerSocket serverSocket = new ServerSocket(80)
如果运行时无法绑定到80端口,则会抛出BindException.原因->
1.端口已经被其他服务器进程占用
2.某些os中,如果没有以超级用户的身份来运行server,则os不允许绑定到1-1023之间的端口
b. port设为0->表示由os来为server分配一个任意可用的端口->匿名端口 ->
->大部分服务器需要使用明确的端口.->因为client程序需要事先知道server端口,才能方便访问server.
-> 匿名端口一般适用于server与client之间的临时通信,通信结束则断开连接,且SeverSocket占用的临时端口也被释放.{@link ServerSocket#getLocalPort()}
->如ftp协议->两个并行的TCP连接->控制连接(21端口)和数据连接.
a. TCP client创建一个监听匿名端口的ServerSocket.再把这个ServerSocket监听的端口好发送给TCP服务器.然后TCP服务器主动请求与client连接.此连接用于和服务器建立临时的数据连接.这种方式就是用了匿名端口.
3.设定client 连接请求队列的长度
1.server运行时,可能会同时监听到多个client的连接请求.
2.管理client连接请求的任务是由os来完成的.os将这些连接请求存储在一个先进先出队列中.
3.许多os限定了队列的最大长度.一般为50.
4. 当队列中的连接请求达到了队列的最大容量时,server进程所在的主机会拒绝新的连接请求 .->只有当server进行通过ServerSocket#accept从队列中取出连接请求->队列腾出空位->队列才能继续加入新的连接请求.
5. 对于client来说,如果其连接请求被加入的server的连接队列中,就意味着client与server连接建立成功->client进程从Socket的构造方法中返回
->如果client进程发出的连接请求被server拒绝,则Socket构造方法抛出ConnectionException.
6.ServerSocket构造中的 backlog 参数用来显示的设置连接请求队列的长度,其将覆盖os限定的队列的最大长度.->注意依然采用os限定的队列的长度的情况:
1.backlog参数的值大于os限定的队列的最大长度.
2.backlog参数的值小于或等于0
3.其构造方法中未设置backlog参数.
4.设定绑定的ip地址
1.如果主机只有一个ip地址,默认情况下,server程序与其绑定.
2. ServerSocket构造方法可显示的指定服务器要绑定的ip地址 ,该构造方法适用于具有多个ip地址的主机.如一个主机具有两个网卡.一个用于连接Internet,以一个用于连接本地局域网.如果server仅仅被本地局域网访问则可显示指定绑定的ip地址.
5.默认构造方法的作用
1.ServerSocket有一个 不带参数 的构造方法->其创建的ServerSocket不与任何端口绑定->需调用bind与特定端口绑定.
2.其用于 在于允许server在绑定到特定端口之前,可先设置SerevrSocket的一些选项 ->因为一旦与特定端口绑定,有些选项就不能再用.
如:
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);//先设置serverSocket的选项,该选项必须在bind之前才有效
serverSocket.bind(new InetAddress(8000));//再执行绑定端口
2.接收和关闭与客户的连接
1.ServerSocket# accept ->从连接请求队列中取出一个client的连接请求->创建于client连接的Socket对象,返回->
2.如果队列中没有连接请求 ,accept方法则会一直等待, 知道接收了连接请求才返回
3.->server从Socket对象获得输入输出流->与client交换数据->当server正在进行发送数据的操作时->client断了连接->server抛出IOException的子类SocketException:
java.net.SocektException: Connection reset by peer.
->这只是 server与单个client通信中的异常,这种异常应该被捕获,使得server能继续与其他client通信.
4.-> 单线程server 采用的通信流程:
public void service()
{
while(true)
{
Socket socket = null;
try
{
socket = serverSocket.accept();//从连接队列中取出一个连接
//接收和发送数据
...
}
catch(IOException e)
{
//这只是与单个client通信时遇到的异常,可能是由于client过早断开连接引起的
//该异常不应该中断整个while循环
e.printStackTrace();
}
finally // 不管与client是通信正常结束还是异常结束,最后都关闭socket,断开与client的连接
{
try
{
if(socket != null)
{
socket.close();//与一个client通信结束后,要关闭socket
}
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
}
3.关闭ServerSocket:
1.ServerSocket#close->使得server释放占用的端口->断开与所有client的连接.
2.当一个server程序运行结束时,即使没有执行close方法,os也会释放这个server占用的端口->server并不一定要在结束之前执行其close方法.
3.某些情况,如果希望及时释放server端口,以便其他程序占用端口,则可显示调用ServerSocket#close方法.
4.ServerSocket#isClosed判断ServerSocket是否关闭->只有执行了ServerSocket#close后,其才返回true.->即使ServerSocket还未与特定端口绑定,isClosed亦返回false.
5. isBound 判断ServerSocket是否已经与一个端口绑定.->只要ServerSocket与一个端口绑定,即使已关闭->isBound也返回true.
6.判断一个ServerSocket是否已经与一个特定端口绑定且还没有被关闭:
boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed()
4.获取ServerSocket的信息
1.public InetAddress getInetAddress()//获得server绑定的ip地址
2.public int getLocalPort()// 获得server绑定的端口
->端口设为0,os为server分配一个匿名端口
3.FTP:
1.ftp使用两个并行的tcp连接,一个是控制连接,一个是数据连接.
2.控制连接用于在client和server之间发送控制信息,如用户名和口令,改变远程目录的命令或上传和下载的命令.
3.数据连接用于传送文件.
4.tcp server在21端口监听控制连接.
5.数据连接的建立两种方式:
1.tcp server在20端口上监听数据连接,tcp client主动请求建立与该端口的连接
2. 匿名端口:tcp client->创建匿名端口的ServerSocket->#getLocalPort发送到server.->tcp server主动请求建立与client的连接.
->client使用匿名端口,用于和server建立临时的数据连接
->实际应用中,server也可使用匿名端口.
5.ServerSocket选项
1. SO_TIMEOUT:表示等待client连接的超时时间.
1.public void setSoTimeout(int timeout) throws SocketException
2.public int getSoTimeout() throws IOException
3.该选项表示ServerSocket的accept方法的等待client连接的超时时间.->ms
4.选项为0,表示永远不会超时,也是其默认值.
5.ServerSocket#accept->如果连接请求队列为空,server就会一直等待->直到收到了client连接才从accept返回->设定了超时时间,如果等待时间超过了超时时间,则抛出SocketTimeoutException.->InterruptedException的子类.
6.Server执行 serverSocket#accept方法时,等待client连接的过程称之为阻塞.
2. SO_REUSEADDR :表示是否允许重用服务器所绑定的地址.
1.public void setReuseAddress(boolean on) throws SocketException
2.public boolean getReuseAddress() throws SocketException
3.同Socket#SO_REUSEADDR->决定如果网络上仍有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到旧的ServerSocket同样的端口上.该选项的默认值与os有关,有的os允许重用端口,有的os不允许重用.
4.ServerSocket关闭时,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络发过来的延迟数据,然后再释放端口.
5.许多server程序使用固定端口。Server程序关闭后->有可能其端口还被占用一段时间->如果此时在同一个主机上重启server程序->由于端口被占用,使得server程序无法绑定到该端口->server启动失败,抛出BindException.
Exception in thread "main" java.net.BindException: Address already in use:JVM_Bind.
6.为确保一个进程关闭ServerSocket后,即使os还未释放端口,同一个主机的上其他进程可以立刻重用该端口->ServerSocket#serReuseAddress(true)
if(!serverSocket.getReuseAddress()){serverSocket.setReuseAddress(true)}
7.必须在serverSocket还未绑定到一个本地端口前调用.否则无效.->两个公用一个端口端口的进程必须都调用setReuseAddress(true)->一个进程关闭ServerSocket后,另一个进程可立即重用相同端口
3. SO_REVBUF :表示接收数据的缓冲区的大小.
1.public void setReceiveBufferSize(int size) throws SocketException
2.public int getReceiveBufferSize() throws SocketException
3.该选项表示Server用于接收数据的缓冲区的大小,字节为单位.(传输大的连续的数据块,http/ftp可以使用较大的缓冲区,减少传输数据的次数,提高传输数据的效率;对于交互频繁且单次数据量较小的通信 telnet和网络游戏,则应该采用较小的缓冲区,确保及时把小批量的数据发送给对方)
4.该默认值与os有关.->如果要设置超过64kb的缓冲区必须在serverSocket绑定到特定端口前设置才有效.
5.执行ServerSocket#setReceiveBufferSize->相当于对所有由serverSocket#accept返回的Socket设置接收数据的缓冲区的大小.
4.public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)->同Socket#setPerformancePreferences->用于设定连接时间,延迟和带宽的相对重要性.
6. 创建多线程的服务器
1.chap1#EchoServer有一个很大的问题就是无法同时与多个client通信.->
1.当前这个EchoServer的问题是只能连接一个Client,只有当前的client输入bye,结束连接的时候,才会处理下一个client
2.原因在于service方法,while(br.readLine() != null),这里,当Client没有输入数据时,线程则挂起,阻塞,等待client输入
->如果同时又多个client请求.这些client则必须排队等待.
2.许多实际应用要求server具有同时为多个client提供服务器的能力-如http服务器.
3. 衡量server具有同时响应多个客户的能力-并发性能
1.能同时接收并处理多个client连接
2.对于每个client,都会迅速给与响应
4.用多个线程来同时为多个client提供服务,这是提高server的并发性能的最常用的手段.
5.
a .为每个客户分配一个工作线程 .伪代码:
public void service()
{
while(true)
{
ServerSocket socket = null;
try
{
socket = serverSocket.accept();//接收client连接
//EchoHandler实现了Runnable接口,其run负责与单个client通信.通信结束则断开连接.工作线程也会自然终止.
Thread workThread = new Thread(new EchoHandler(socket));//创建一个工作线程
workThread.start();//启动工作线程
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
b.该实现缺点:
1. server创建和销毁工作线程的开销 ,包括所花费的时间和系统资源很大.->如果server与多client通信并且每个client的通信时间都很短->那么server为client创建新线程的开销比实际与client通信的开销还要大.
2.除了创建和销毁线程的开销, 活动的线程也消耗系统资源.每个线程本身会占用一定的内存 ,每个线程约1M.如果有大量client连接server,就必须创建爱你大量工作线程->消耗了大量内存,导致系统的内存空间不足 .(-Xss,设置每个线程的堆栈大小)
3.如果线程数目固定,并且每个线程都有很长的生命周期,则 线程切换 也是相对固定的.->不同的os有不同的切换周期,一般在20ms.这里所说的线程切换是在Java虚拟机以及底层os的调度之下,线程之间转让cpu的使用权.如果频繁创建和销毁线程,则导致频繁切换线程;因为一个线程被销毁后,必须要把cpu转让给另一个已就绪的线程,使该线程或的运行机会.这种情况下,线程之前的切换不再遵循系统的固定切换周期,切换线程的开销甚至比创建及销毁线程的开销还要大.
->引入线程池.
6.线程池为线程生命周期开销问题和系统资源不足问题提供了解决方案:
1 .线程池预先创建一些工作线程->其不断的从工作队列中取出任务->执行任务->工作线程执行完一个任务->继续执行工作队列的下一个任务->
2.线程池减少了创建和销毁的次数,每个工作线程都可以被一直重用,能执行多个任务
3.可根据系统的负载能力,方便的调整线程池中线程的数目->防止因为消耗过量系统资源而导致系统崩溃.
c.使用java.util.concurrent包提供了现成的线程池的实现.->比ThreadPoolImpl的要健壮而且功能更强大.
1.public interface Executor #void execute(Runnable command)
2.public interface ExecutorService extends Executor
1.void shutdown ()
2.List<Runnable> shutdownNow ()
3.boolean awaitTermination (long timeout, TimeUnit unit) throws InterruptedException
4.<T> Future<T> submit(Callable<T> task)
5.<T> Future<T> submit (Runnable task, T result)
6.Future<?> submit (Runnable task)
7.<T> List<Future<T>> invokeAll (Collection<? extends Callable<T>> tasks) throws InterruptedException
8.<T> T invokeAny (Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException
9.boolean isTerminated ()
3.public class Executors
-> Factory and utility methods
1. public static ExecutorService newCachedThreadPool () {
return new ThreadPoolExecutor ( 0, Integer.MAX_VALUE,
60L , TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());}
2.public static ExecutorService newFixedThreadPool (int nThreads) {
return new ThreadPoolExecutor( nThreads, nThreads,
0L , TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());}
3. public static ExecutorService newSingleThreadExecutor () {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor ( 1, 1,
0L , TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));}
-> Note however that if this single thread terminates due to a failure during execution prior to shutdown, a new one will take its place if needed to execute subsequent tasks.
->与 newFixedThreadPool(1)区别 ->Unlike the otherwise equivalent newFixedThreadPool(1) the returned executor is guaranteed not to be reconfigurable to use additional threads.
->The difference is (only) that the SingleThreadExecutor cannot have its thread size adjusted later on, which you can do with a FixedThreadExecutor by calling ThreadPoolExecutor#setCorePoolSize (needs a cast first).->((ThreadPoolExecutor)newFixedThreadPool(1)).setCorePoolSize(3)->
->ThreadPoolExecutor->
protected void finalize() { shutdown();}->因为DelegatedExecutorService持有了ThreadPoolExecutor引用->所以FinalizableDelegatedExecutorService->
->static class FinalizableDelegatedExecutorService extends DelegatedExecutorService->#finalize->super.shutdown->即在执行finalize的时候调用shutdown->因为其持有ExecutorService引用,所以其不能finalize->只能持有者调用finalize->
4. public ThreadPoolExecutor ( int corePoolSize,int maximumPoolSize, long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue ) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);}
4.public abstract class AbstractExecutorService implements ExecutorService
5.public interface ScheduledExecutorService extends ExecutorService
6.public class ThreadPoolExecutor extends AbstractExecutorService
7.public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService
7.使用线程池的注意事项:
1. 死锁
1.A线程持有对象X的锁并等待对象Y的锁->B线程持有对象Y的锁并等待对象X的锁->A,B线程都不释放自己持有的锁->并且等待对方的锁->导致两个线程永远等待下去->产生死锁
2.线程池死锁->所有工作线程都在执行各自任务时被阻塞->等待某个任务A的执行结果->而任务A仍在工作队列中->没有空闲线程->A一直不能被执行->是的线程池的所有工作线程都永远阻塞->死锁->
2. 系统资源不足
线程池线程数目多->消耗内存和其他系统资源.->影响系统性能.
3. 并发错误
ThreadPoolImpl的工作队列依靠wait和notify使工作线程即使取得任务.->如果编码不正确,可能会丢失通知->导致工作线程一直保持空闲状态->无视工作队列需要处理的任务->使用这些方法必须格外小心->最好使用现有的,比较成熟的线程池->如concurrent包中的线程池类.
4. 线程泄露
1.对于工作线程数目固定的线程池->如果工作线程在执行任务抛出RuntimeException或Error->并且这些异常或错误没有被捕获->工作线程就会异常终止->线程池失去了一个工作线程->如果所有的工作线程都异常终止->线程池就为空->
2.工作线程在执行一个任务被阻塞->如等待用户输入数据->但由于用户一直不输入数据->导致工作线程一直被阻塞->这样的工作线程名存实亡->不执行任何任务了->如果线程池中所有工作线程都处于这样的阻塞状态->线程池无法处理新加入的任务
5.任务过载
当工作队列中有大量排队等待执行的任务时->这些任务本身可能消耗太多系统资源而引起系统资源缺乏.
8.使用线程池 遵循原则:
1.如果任务A在执行过程中需要同步等待任务B的执行结果->任务A不适合加入到线程池的工作队列->如果把像任务A一样的需要等待其他任务执行结果的任务加入到工作队列中->可能会导致线程池的死锁.
2.如果执行某个任务可能会阻塞并且会长时间阻塞->应该设定超时时间->避免工作线程永久的阻塞下去->导致线程泄露->
->服务器程序中->线程等待client连接 或者 等待client发送的数据时都可能会阻塞->
1.ServerSocket#setSoTimeout(int timeout)->设定等待client连接的超时时间
2.对于每个与client连接的Socket->Socket#setSoTimeout(int timeout)->设定等待client发送数据的超时时间.
3. 了解任务的特点:
1.分析任务时经常会执行阻塞的io操作->还是执行一直不会阻塞的运算操作->前者时断时续的占用cpu,而后者对cpu具有更高的利用率->预计完成人呢无的大概时间->是短时间任务还是长时间任务?->根据任务的特点->对任务进行分类->不同类型的任务加入到不同线程池的工作队列中->根据任务的特点->分别调整每个线程池.
4. 调整线程池的大小:
1.线程池的最佳大小主要取决于系统的可用cpu的数目以及工作队列中任务的特点.
2.如一个具有n个cpu的os上只有一个工作队列且其中全部是运算性质的任务,即不会阻塞的任务,则线程中具有n或n+1个工作线程时,一般会获得最大的cpu利用率。
3.如果工作队列中包含会执行I/O操作并常常阻塞的任务,则要让线程池的大小超过可用cpu的数目,因为并不是所有工作线程都一直在工作。选择一个典型的任 务,然后估计在执行这个任务的过程中,等待时间(WT)与实际占用CPU进行运算的时间(ST)之间的比例WT/ST。对于一个具有N个CPU的系统,需 要设置大约 N×(1+WT/ST) 个线程来保证CPU得到充分利用。 注-(n + 平均阻塞的线程)
4.CPU利用率不是调整线程池大小过程中唯一要考虑的事项。随着线程池中工作线程数目的增长,还会碰到 内存或者其他系统资源的限制,如套接字、打开的文件句柄或数据库连接数目等 。要保证多线程消耗的系统资源在系统的承载范围之内
5. 避免任务过载 。服务器应根据系统的承载能力,限制客户并发连接的数目。当客户并发连接的数目超过了限制值,服务器可以拒绝连接请求,并友好地告知客户:服务器正忙,请稍后再试.
9.关闭服务器
1.之前的EchoServer都无法关闭自身->只能靠os强制终止->处理方式虽然简单,但是会导致服务器正在执行的任务被突然中断->server处理的任务重要->不允许被突然中断->server需在恰当的时刻关闭自己.
注:1.很多游戏服务器程序采用ShutdownHooker来进行jvm进程退出的清理工作,如linux下kill的时候会调用钩子.
2.本人倾向于在游戏服务器正常的关闭自己->理想的情况下是向游戏服务器发送shutdown命令,然后游戏服务器执行清理工作,所有线程执行shutdown->jvm进程自然退出.
部分源码:
























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































