文章目录
在客户端服务端通信模式中,服务器端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户端的连接请求。
1 构造ServerSocket
ServerSocket的构造方法有以下几种重载形式:
ServerSocket的构造方法有以下几种重载形式:
ServerSocket()throws IOException
/***
port: 服务器要监听的端口号
*/
ServerSocket(int port) throws IOException
/***
port: 服务器要监听的端口号
backlog: 指定客户连接请求队列的长度
*/
ServerSocket(int port, int backlog) throws IOException
/***
port: 服务器要监听的端口号
backlog: 指定客户连接请求队列的长度
bindAddr: 指定服务器要绑定的IP地址
*/
ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
1.1 绑定端口
通过构造函数的port参数绑定监听的端口,会抛出IOException,更确切的说,是抛出BindException,它是IOException的子类,一般由于一下原因造成:
- 端口已经被占用
- 个别操作系统,需要超级管理员用户的身份才可以绑定到1~1023之间的端口
另外,如果把端口设置为0,则表示系统有操作系统随机分配一个可用的端口。也被称为匿名端口。
@Test
public void testBindPort() throws IOException {
// 设置为匿名端口
ServerSocket serverSocket = new ServerSocket(0);
// 获取分配的端口
int localPort = serverSocket.getLocalPort();
System.out.println("port: " + localPort);
}
多数服务器会监听固定的端口,这样才便于客户程序访问服务器。匿名端口一般适用与客户端之间的临时通信,通信结束后就断开连接,并且serversocket绑定的临时端口也会被释放
1.2 设定客户连接请求队列的长度
当服务器运行时,可能会同时监听到多个客户端的连接请求,管理客户端连接请求的任务是由于操作系统来完成的,操作系统把这些请求存储在一个FIFO(先进先出)的队列中。许多操作系统都限定了队列的最大长度,一般为50。当队列满了,服务器进程就会拒绝新的连接请求,这个时候客户端会抛出ConnectionException
ServerSocket构造方法的backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。
值得注意的是,在以下几种情况,仍然会采用操作系统限定的队列的最大长度:
- backlog参数的值大于操作系统限定的队列的最大长度。
- backlog参数的值小于或等于0。
- 在ServerSocket构造方法中没有设置backlog参数。
new ServerSocket(80,2);
验证一下系统的默认值
- 客户端,模拟发送100个请求
import java.io.IOException;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
final int port = 9999;
final String host = "localhost";
final int len = 100;
// 尝试建立100次连接
Socket[] sockets = new Socket[len];
for (int i = 0; i < len; i++) {
sockets[i] = new Socket(host, port);
System.out.println("第" + (i + 1) + "次数" + "连接成功");
}
// 关闭连接
for (int i = 0; i < len; i++) {
sockets[i].close();
}
}
}
- 服务端
import java.io.IOException;
import java.net.ServerSocket;
public class Server {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("服务启动");
// 为了服务不挂掉
System.in.read();
}
}
启动服务端和客户端,控制台输出
第1次数连接成功
第2次数连接成功
第3次数连接成功
....
第49次数连接成功
第50次数连接成功
Exception in thread "main" java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method)
可见默认是50次
设置最大连接数为3:
public class Server {
public static void main(String[] args) throws IOException, InterruptedException {
// 设置最大连接数为3
ServerSocket serverSocket = new ServerSocket(9999,3);
System.out.println("服务启动");
// 为了服务不挂掉
System.in.read();
}
}
重新测试:
第1次数连接成功
第2次数连接成功
第3次数连接成功
Exception in thread "main" java.net.ConnectException: Connection refused: connect
- 修改服务端代码,之前的服务端没有处理请求,现在进行处理
public class Server {
public static void main(String[] args) throws IOException, InterruptedException {
// 设置最大连接数为3
ServerSocket serverSocket = new ServerSocket(9999,3);
System.out.println("服务启动");
while (true){
Socket socket = null;
try {
// 获取客户端连接, 就是从连接请求队列中获取一个连接
socket = serverSocket.accept();
System.out.println("new connection from" + socket.getInetAddress() + ":"+ socket.getPort());
}catch (Exception e){
e.printStackTrace();
}finally {
if(socket!=null) socket.close();
}
}
}
}
再次测试,由于这次每来一个请求,服务端都及时处理,没有出现队列满的情况:
第1次数连接成功
第2次数连接成功
第3次数连接成功
第4次数连接成功
...
第99次数连接成功
第100次数连接成功
1.3 绑定ip
如果主机只有一个ip,默认情况下,服务器程序就与该ip绑定。但是也可以通过构造方法绑定指定的ip。
/***
port: 服务器要监听的端口号
backlog: 指定客户连接请求队列的长度
bindAddr: 指定服务器要绑定的IP地址
*/
ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
1.4 无参构造
通过该方法创建的ServerSocket不与任何端口绑定,接下来还需要通过bind()方法与特定端口绑定。
主要的作用是,在绑定端口之前,先对ServerSocket进行设置,因为有些设置,在绑定之后就无法设置了
2 accept方法
这个方法就是从连接请求队列中取出一个客户端请求,然后创建与客户端连接的Socket对象并返回,如果队列里没有请求,这个方法就会一直等待,直到收到了连接请求
Socket socket = serverSocket.accept();
接下来获取输入流和输出流就可以和客户端交互了。当服务器正在进行发送数据操作时候,如何客户端断开了连接,那么服务端会抛出一个异常IOException的子类SocketException
服务端这种异常应该捕获掉,不应该因为这些异常导致服务端挂掉,导致其他客户端不能和服务端通信。
while (true) {
Socket socket=null;
try {
socket = serverSocket.accept(); //从连接请求队列中取出一个连接
System.out.println("New connection accepted " +
socket.getInetAddress() + ":" +socket.getPort());
//接收和发送数据
…
}catch (IOException e) {
//这只是与单个客户通信时遇到的异常,可能是由于客户端过早断开连接引起的
//这种异常不应该中断整个while循环
e.printStackTrace();
}finally {
try{
if(socket!=null)socket.close(); //与一个客户通信结束后,要关闭Socket
}catch (IOException e) {e.printStackTrace();}
}
}
所以上面的代码,被trycatch包起来了,不期望在和某一个客户端交互的是发生异常,而导致服务端挂掉
3 关闭ServerSocket
close()方法会释放服务器程序占用的端口,断开和所有客户端的连接。
4 获取ServerSocket的信息
ServerSocket的以下两个get方法分别获得服务器绑定的IP地址,以及绑定的端口:
public InetAddress getInetAddress()
public int getLocalPort()
4 ServerSocket的配置项
- SO_TIMEOUT:表示等待客户连接的超时时间。
- SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。
- SO_RCVBUF:表示接收数据的缓冲区的大小
4.1 SO_TIMEOUT
等待客户连接的超时时间。以毫秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。
当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那么当服务器等待的时间超过了超时时间,就会抛出SocketTimeoutException,它是InterruptedException的子类。
InterruptedException可以看出底层用的wait方法
// 设置该选项:
public void setSoTimeout(int timeout) throws SocketException
// 读取该选项:
public int getSoTimeout () throws IOException
4.2 SO_REUSEADDR选项
这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数据向旧的ServerSocket传输,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口。他的默认值和操作系统有关,有的系统允许重用端口,有的则不允许。
当serversocket关闭时,如果网络上还有发送到这个serversocket的数据,那么这个serversocket不会立刻释放端口,而是会等待一段时间,确保接收到了网络上发送过来的数据,然后释放端口。
为了确保一个进程关闭了ServerSocket后,即使操作系统还没释放端口,同一个主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket的setResuseAddress(true)方法。
并且需要在绑定端口之前设置:
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(8000));
4.3 O_RCVBUF选项
SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(比如基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信(比如Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。
// 设置该选项:
public void setReceiveBufferSize(int size) throws SocketException
// 读取该选项:
public int getReceiveBufferSize() throws SocketException
建议在绑定端口前进行设置,如果设置的大小超过了64k的缓冲区,就必须在绑定之前设置,才有效。
4.4 设定连接时间、延迟和带宽的相对重要性
该方法的作用与Socket的setPerformancePreferences()方法的作用相同,用于设定连接时间、延迟和带宽的相对重要性。
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)