服务端Socket
1、什么是服务端socket?
服务端socket是一个运行在服务器上的,监听入站TCP连接的连接。每个服务端socket监听服务器上的一个特定端口。当远程主机上的一个客户端尝试连接这个端口时,服务端就被唤醒,协商建立客户端和服务器之间的连接,并返回一个常规的Socket对象,表示两台主机之间的socket。换句话说,服务端socket等待连接,而客户端发起连接。一旦serverSocket建立了连接,服务器会使用一个常规的socket对象向客户端发送数据。数据总是通过常规的socket传输。
2、服务端socket的生命周期。
(1)、使用一个ServerSocket()构造函数在一个特定端口创建一个新的ServerSocket。
(2)、ServerSocket使用其accept()方法监听这个端口的入站连接。accept()会一直阻塞,直到客户端尝试建立连接,此时accept()将返回一个连接客户端和服务器的scoket对象。
(3)、根据服务为的类型,会调用socket的getInputStream()方法或getOutputStream()方法,或者这两个方法都调用,以获得客户端的输入和输出流。
(4)、服务器和客户端根据已经协商好的协议进行交互,直到要关闭连接。
(5)、服务器或客户端关闭连接。
(6)、服务端返回到步骤(2),等待下一次连接。
注意:结束处理时一定要关闭socket。因为客户端不能依赖连接的另外一端关闭的socket,对于服务器尤其如此。客户端可能超时或崩溃;用户可能取消事务;网络可能在流量高峰期间瘫痪;黑客可能发动拒绝服务攻击。出于诸如此类的众多原因,你不能依赖于客户端关闭socket,即使协议有这个要求也不能完全相信客户端一定会关闭socket。
3、多线程服务器与线程池服务器
由于操作系统把指向某个特定端口的入站连接请求存储在一个先进先出队列中。默认地,java将这个队列的长度设置为50,不同的操作系统会有所不同。在这些操作系统中,java服务器socket的队列长度将是操作系统所允许的最大值(小于或等于50)。队列中填入的未处理的连接达到最大时,主机会拒绝这个端口上的额外的连接,直到队列腾出新的位置位置。很多客户端在首次连接被拒绝外还会多次尝试建立连接,因此,能够比新连接到来的速度更快的清空队列尤为重要。
这里可以采用一个多线程的服务器为每一个连接提供一个线程,与接收入栈的放入队列的那个线程分开,这样可以防止一个慢客户端阻塞所有其他客户端。(这里开始不是很懂,请教了一下学长,打个比方:如果在只有主线程的情况下,队列中所有已经出队列并建立起连接的请求的执行顺序也是按照连接的顺序执行,也就是说如果一个客户端跟服务器建立起连接后,在发送数据的过程中因为某些原因,比如路由过多,某一路由上的网络阻塞导致数据的传输很久才能到达对方,对于其他的连接请求就必须等待较长的时间,而越是在后面的连接请求执行的时间越是长,这样使得后面接入的连接请求响应速度十分缓慢,造成不好得用户体验;并且在主线程中如果主线程既要处理客户端发送过来的连接请求,又要处理已经连接并在传输数据的请求,当请求的数量十分庞大时,造成如I/O读取的效率非常低。而多线程服务器将接入请求创建连接和处理业务逻辑相分离,使得两个部分都能够得到运行,提高了CPU的利用率和I/O读取以及用户体验度等)
以上示例使用try-with-resources来自动关闭服务器socket。不过,对于服务器socket接受的客户端socket这里有意没有使用try-with-resources。这是因为,客户端socket避开了try块,而放在一个单独的线程中。如果使用了try-with-resources,主线程一旦到达while循环末尾就会关闭socket,而此时新生成的线程可能还没有使用完这个socket,因为连接是双向的,所以不会关闭客户端socket,只会关闭服务端socket。
这个服务器上确实有可能发生一种拒绝服务器攻击,由于示例会为每一个连接生成一个新的线程,大量几乎同时的入站连接可能导致极大的线程数量,最终导致jvm虚拟机会耗尽内存而崩溃。因此采用一个固定的线程池来限制可能的资源使用,不论负载多大,都不会崩溃。
4、用socket写入服务器
大多数协议要求服务器同时读写客户端。与之前一样,先是创建一个连接,然后获取InputStream读取客户端。另外使用OutputStream写入客户端。关键是要理解协议:明确何时写入和何时读取。
5、关闭服务器Socket
如果使用完一个服务器Socket,就应当将他关闭,特别是当程序还要继续执行一段时间时更是如此。这会释放端口,使其他希望使用这个端口的横须可以使用。不要把关闭ServerSocket和关闭socket混淆。关闭ServerSocket会释放本地主机的一个端口,允许另一个服务器绑定到这个端口。它还会中断该ServerSocket已经接受的目前处于打开状态的所有Socket。
6、日志
服务器要在无人看管的情况下运行很长时间。通常需要在很久之后对服务器中发生的情况进行调试,这很重要。由于这个原因,建议在存储服务器日志,至少要存储一段时间的日志。日志记录中通常包含两个主要内容,一个是请求,一个是服务器错误。可以采用log4j或者Apache Commons Loggin之类的第三方日志库进行记录。
7、构造服务器Socket
ServerSocket(int port);
ServerSocket(int port, int queueLength);
ServerSocket(int port, int queueLength,InetAdress bindAddress);
ServerSocket();
8、Socket选项
对于服务端Socket有以下三个选项,SO_TIMEOUT(服务器超时时间),SO_REUSEADDR(确定是否允许一个新的Socket是否绑定到之前使用过的一个端口,而此时可能还有一些发送到原Socket的数据正在网络上传输,此时要注意新端口是否会接收到这些没有用的数据,这跟客户端Socket十分类似),SO_RCVBUF(设置服务端接收的客户端Socket默认接收缓冲区大小)。