套接字Socket
Socket 通常也称作套接字,网络上的两个程序通过一个双向链路实现数据的交换,这个双向链路的一端称为一个 Socket,由IP地址和端口号唯一确定。应用程序通过套接字向网络发出请求或者应答网络请求。
Socket 是对 TCP/IP 协议的封装,其本身并不是协议,而是一个调用接口(API)。大多数的API提供者(如操作系统,JDK)往往会提供基于这种概念的接口。
TCP/IP 中主要 socket 类型为流套接字(使用 TCP 协议)和数据报套接字(使用 UDP 协议)。
Java 为 TCP 协议提供了两个类:Socket 类和 ServerSocket 类。Socket 实例代表客户端,ServerSocket 实例代表服务器端,客户端与服务器端的关系是一对多的关系。
客户端与服务端的连接步骤
客户端执行如下三步操作(参考Java TCP Socket 编程):
- 创建 Socket 实例:构造函数向指定的远程主机和端口建立 TCP 连接;
- 通过 Socket 的 I/O 流与服务端通信;
- 通信完成后,使用 Socket 类的 close() 方法关闭连接。
服务端执行如下两步操作:
- 创建一个 ServerSocket 实例并指定本地端口,用来监听客户端在该端口发送的 TCP 连接请求;
- 调用 ServerSocket 的 accept()方法获取客户端连接,并通过其返回值创建一个 Socket 实例;
- 通过 Socket 实例的 I/O 流与客户端通信;
- 通信完成后,使用 Socket 类的 close() 方法关闭连接。
一对一Socket Demo
简单的 socket 通信实验可以在自己的电脑上就可以完成。如下是一个一对一的 socket 通信例子。
客户端:
public class ClientSocket {
private static String ip = "127.0.0.1";
private static int port = 2017;
public static void main(String[] args) throws IOException {
ClientSocket client = new ClientSocket();
client.init();
}
public void init() throws IOException {
System.out.println("客户端初始化完成!");
Socket client = new Socket(ip, port);
client.setSoTimeout(5000);//超时时间为5s
System.out.println("服务器端初始化完成!");
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream());
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请输入要发送的数据:");
// out.write("你好\n");
out.println(sc.nextLine());
out.flush();
String msg = in.readLine();
if (msg != null) {
if (msg.equals("end")) {
out.println("end");
out.flush();
System.out.println("本次连接结束!");
break;
}
System.out.println(msg);
}
}
sc.close();
out.close();
in.close();
client.close();
}
}
服务器端:
public class SocketServer {
private static final int SERVER_PORT = 2017;
public static void main(String[] args) throws IOException {
SocketServer server = new SocketServer();
server.serverInit();
}
public void serverInit() throws IOException {
@SuppressWarnings("resource")
ServerSocket socket = new ServerSocket(SERVER_PORT);
Socket server = socket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
PrintWriter out = new PrintWriter(server.getOutputStream());
while (true) {
String msg = in.readLine();
if (msg != null) {
if (msg.equals("end")) {
out.println("end");
out.flush();
break;
}
out.println("服务器:好的!");
out.flush();
}
}
out.close();
in.close();
server.close();
}
}
输出结果:
客户端初始化完成!
服务器端初始化完成!
请输入要发送的数据:hello
服务器:好的!
请输入要发送的数据:end
本次连接结束!
注意点
- 必须先启动服务器端,再启动客户端
- 向另一端写数据时如果用的是 PrintWriter 或其它带缓冲的IO流时必须调用 flush() 方法以立即刷新数据
- 服务器端和客户端的套接字要对应
- 如果用的是 write() 方法写入字符串,另一端采用 readline() 方法读取时,需要在字符串最后加上换行符(参考代码中被注释的语句),println() 方法已经自动帮我们添加了换行符
- 如果提示端口已被占用,查看 console 中是否有未结束的进程(一般有服务器端和客户端两个 console),或者查看任务管理器结束多余的 Java(TM) Platform SE binary 进程,如果还不行就换个端口
非并发多对一Socket Demo
虽然上面的程序实现了 Socket 通信的功能,可是只能满足一个客户端与服务器端通信的要求。当一个客户端发送 end 后,服务器端也被终止了。
为了满足同时多台客户端连接服务器端的要求,我们需要对服务器端做一些改进。
public class SocketServer2 {
private static final int SERVER_PORT = 2017;
public static void main(String[] args) throws IOException {
SocketServer server = new SocketServer();
server.serverInit();
}
public void serverInit() throws IOException {
@SuppressWarnings("resource")
ServerSocket socket = new ServerSocket(SERVER_PORT);
while (true) {
Socket server = socket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
PrintWriter out = new PrintWriter(server.getOutputStream());
while (true) {
String msg = in.readLine();
if (msg != null) {
if (msg.equals("end")) {
out.println("end");
out.flush();
break;
}
out.println("服务器:好的!");
out.flush();
}
}
out.close();
in.close();
server.close();
}
}
}
客户端代码不变。可以看到,只是又加了一层 while 循环。这样当一个客户端发送 end 后,其它客户端仍能连接到服务器端。
并发多对一Socket Demo
但现在又有一个问题,只有当前正在通信的客户端发送 end 后,其它客户端才能与服务器端通信,也就是其它客户端会被阻塞。原因是 ServerSocket的accept() 方法是阻塞的。
如何让多个客户端同时与服务器端通信呢?这就要靠多线程了。
public class SocketServer3 {
private static final int SERVER_PORT = 2017;
public static void main(String[] args) throws IOException {
SocketServer server = new SocketServer();
server.serverInit();
}
public void serverInit() throws IOException {
@SuppressWarnings("resource")
ServerSocket socket = new ServerSocket(SERVER_PORT);
while (true) {
Socket server = socket.accept();
new Thread(new Dispatch(server)).start();
}
}
static class Dispatch implements Runnable {
Socket innerServer;
Dispatch(Socket socket) {
this.innerServer = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(innerServer.getInputStream()));
PrintWriter out = new PrintWriter(innerServer.getOutputStream())) {
while (true) {
String msg = in.readLine();
if (msg != null) {
if (msg.equals("end")) {
out.println("end");
out.flush();
break;
}
out.println("服务器:好的!");
out.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
将业务逻辑封装到一个内部类中,内部类实现了 Runnable 接口,当然也可以选择继承 Thread。资源的关闭选择使用了 try-with-resources 处理。这样就实现了多个客户端可以同时与服务器端通信的目的。
事实上,当客户端逐渐增多的时候,建立的线程也逐渐增多,而服务器的线程是有限的,过多的线程造成的线程间切换的消耗也会相当的大。因此这种方式在并发量大的场景下无法承载。可以使用线程池来解决这个问题。
线程池Socket Demo
在 java.util.concurrent 包下,提供了一系列与线程池相关的类。一般通过工具类 Executors 的静态方法来获取线程池。
Executors 提供了四种线程池,这里选择了 FixedThreadPool。它会创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。此时如果有新任务到来,会将任务加入到无界队列 LinkedBlockingQueue 等待。
线程池的大小要根据任务特性决定,一般如果是CPU密集型,线程数量为N+1(N为CPU核数),如果是IO密集型,线程数量为2*N。这里我用的线程池大小是8。
示例如下:
public class SocketServer {
private static final int SERVER_PORT = 2017;
public static void main(String[] args) throws IOException {
SocketServer server = new SocketServer();
server.serverInit();
}
public void serverInit() throws IOException {
@SuppressWarnings("resource")
ServerSocket socket = new ServerSocket(SERVER_PORT);
ExecutorService exec = Executors.newFixedThreadPool(8);
while (true) {
Socket server = socket.accept();
exec.execute(new Thread(new Dispatch(server)));
}
}
static class Dispatch implements Runnable {
Socket innerServer;
Dispatch(Socket socket) {
this.innerServer = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(innerServer.getInputStream()));
PrintWriter out = new PrintWriter(innerServer.getOutputStream())) {
while (true) {
String msg = in.readLine();
if (msg != null) {
if (msg.equals("end")) {
out.println("end");
out.flush();
break;
}
out.println("服务器:好的!");
out.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
考虑一个聊天服务器,可能有上千个客户端同时连接到服务器,但是在任何时刻只有非常少量的消息需要读取和分发,如果采用线程池会非常浪费资源,这时候采用NIO会有更好的效果。