ServerSocket API
API:java.net.ServerSocket 1.0
- ServerSocket(int port, int backlog)
创建一个监听端口的服务器套接字 - ServerSocket() 1.4
创建一个未绑定的服务器套接字 - void bind(SocketAddress endpoint, int backlog) 1.4
把服务器套接字绑定到指定套接字地址上
注意
bind
方法常常在调用无参构造函数 ServerSocket()
之后使用。如果 bind 和其他有参构造函数一起使用,会产生报错。以下错误示范:
ServerSocket serverSocket = new ServerSocket(8081, 2); // 使用有参构造函数,创建一个监听端口的服务器套接字
serverSocket.bind(new InetSocketAddress(8081), 1); // 这里再调用bind方法,重复绑定,产生异常!
如下图所示:
backlog 参数
backlog 参数是套接字上请求的最大挂起连接数。它的确切语义是特定于实现的。
backlog 是请求的 incoming 连接队列的最大长度。
创建 ServerSocket 并绑定端口:
Socket 服务端代码
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080), 1);
System.in.read(); // 不让服务端关闭
}
}
这里使用 telnet 127.0.0.1 8080
打开两个 Windows Telnet 客户端,根据 WireShark 的抓包结果如下:
- 端口号为 5608 的第一个 Telnet 客户端,经过三次握手,顺利地和服务器建立的连接并保持了连接
- 端口号为 5620 的第二个 Telnet 客户端,首先发送了第一次握手报文(SYN),但是服务器因为设置了 backlog 为 2,因此直接给客户端返回 (RST) 报文。客户端尝试重传 (报文内容和第一次握手时的报文一模一样),尝试2次后收到的仍然是RST 报文,就不了了之。
如果改用 Java 客户端,代码如下:
public class Clients {
public static void main(String[] args) throws IOException {
Socket[] clients = new Socket[2];
for (int i = 1; i <= clients.length; i++) {
clients[i-1] = new Socket("127.0.0.1", 8080);
System.out.println("client connection:" + i);
}
}
}
控制台发生报错:
- 第一个客户端 Socket 创建成功,但是第二个客户端的 Socket 被拒绝连接。
因此,在这种情况下,能够成功创建客户端套接字的个数,刚好就是创建 ServerSocket 时候指定的 backlog 的数量。
用 accept 返回 Socket 对象
API:java.net.ServerSocket 1.0
- Socket accept()
等待连接。该方法阻塞(即使之空闲)当前线程直到建立连接为知。该方法返回一个 Socket 对象,程序可以通过这个对象与连接中的客户端进行通信。
我们改造一下 ServerSocket,在 while 循环调用 ServerSocket#accept 方法。
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080), 1);
int acceptCount = 0;
while (true) {
Socket clientSocket = serverSocket.accept();
InetSocketAddress remote = (InetSocketAddress) clientSocket.getRemoteSocketAddress();
System.out.println(remote.getPort());
++acceptCount;
System.out.println("当前客户端连接数:" + acceptCount);
}
}
}
客户端我们也改一下,变成并发量 100 的连接请求
public class Clients {
public static void main(String[] args) throws IOException {
Socket[] clients = new Socket[100];
for (int i = 1; i <= clients.length; i++) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
clients[index-1] = new Socket("127.0.0.1", 8080);
System.out.println("client connection:" + index);
} catch (IOException e) {
e.printStackTrace();
}
}
}, String.valueOf(i)).start();
}
System.in.read();
}
}
经过实验,backlog=1 时, 一次运行结果如下:
多次执行,拒绝连接的数量存在波动。
给服务端加上阻塞
上一个实验中,我们使用 accept 来返回 Socket 对象。我们把套接字从 sync_queue 转移到 accept_queue,这样就可以接收更多的连接了。
但是,如果我们用 sleep 来模拟接收到连接后的收发消息,业务处理的延迟,实验结果又会不同。
带延迟的 SocketServer
public class SocketServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080), 1);
int acceptCount = 0;
while (true) {
Socket clientSocket = serverSocket.accept();
InetSocketAddress remote = (InetSocketAddress) clientSocket.getRemoteSocketAddress();
System.out.println(remote.getPort());
++acceptCount;
System.out.println("当前客户端连接数:" + acceptCount);
Thread.sleep(2000); // 加入延迟时间
}
}
}
同步客户端
public class SyncClients {
private static Socket[] clients = new Socket[100];
public static void main(String[] args) throws Exception {
for (int i = 1; i <= clients.length; i++) {
clients[i-1] = new Socket("127.0.0.1", 8080);
System.out.println("client connection:" + i);
}
}
}
同步连接客户端,套接字是一个接着一个连接的。先完成一组三次握手,再进行第二组三次握手,以此类推。
第一次连接从 sync_queue 转移到 accept_queue,
第二次连接进入到 sync_quque,
第三次连接因为 backlog=1 的缘故,被拒绝连接了,客户端抛出异常。
结果如图所示:
异步客户端
public class Clients {
static Socket[] clients = new Socket[3];
public static void main(String[] args) throws IOException {
for (int i = 0; i < clients.length; i++) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
clients[index] = new Socket("127.0.0.1", 8080);
System.out.println("client connection:" + index);
Thread.sleep(10000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}, String.valueOf(i)).start();
}
System.in.read();
}
}
测试结果并没有像我预料的那样,第三次连接失败
这里我先留个坑,跟网上查询的 sync_queue 理论有些不符合的样子。如果有熟悉底层的大佬可以指点一二。
小贴士
- 如果
SyncClients
中没有加入System.in.read()
代码,客户端程序会停止运行,客户端主动给服务器端发送 RST 报文重置连接。