java 中的 socket

先了解 Java 中的 net包中的主要方法

  1. TCP 相关 (面向连接):
    • ServerSocket: 用于服务器端。它的核心职责是在服务器上监听一个特定的端口,等待客户端的连接请求。
    • Socket: 用于客户端或服务器端。它代表了一个网络连接的两端中的一端。一旦服务器通过 ServerSocket 接受了一个连接,就会得到一个 Socket 对象,该对象代表了与那个特定客户端的专用通信通道。客户端为了发起连接,也会创建一个 Socket 对象。
  2. UDP 相关 (无连接):
    • DatagramSocket: 用于发送和接收UDP数据报。与TCP不同,它不建立持久连接,每个数据包都是独立发送的。
    • DatagramPacket: 代表一个UDP数据报。无论是发送还是接收,数据都必须封装在这个对象里。

对c语言的网络编程socket对比

C 语言函数 (服务器端) Java 对等操作 解释
socket() + bind() new ServerSocket(port) C语言中需要先创建一个套接字描述符,再将它绑定到地址和端口。Java的 ServerSocket 构造函数将这两步合二为一了。
listen() (隐含在 ServerSocket 中) 在C中,listen() 将套接字置为被动监听模式,并指定等待连接的队列大小。在Java中,ServerSocket 对象一旦创建成功,就处于可以接受连接的状态,其构造函数也有一个版本可以指定这个队列大小(backlog)。
accept() serverSocket.accept() 这是最关键的对应。两者都会阻塞程序,直到一个客户端连接进来。两者都会返回一个新的东西来代表这个连接:C返回一个新的文件描述符 (int),Java返回一个新的Socket 对象。核心思想完全一致。
send() / write() socket.getOutputStream().write(byte[]) 通过返回的新 Socket 对象的输出流来发送数据。
recv() / read() socket.getInputStream().read(byte[]) 通过返回的新 Socket 对象的输入流来接收数据。
C 语言函数 (客户端) Java 对等操作 解释
socket() + connect() new Socket(host, port) C语言中客户端先创建一个套接字,再调用 connect 去连接服务器。Java的 Socket 构造函数将这两步合二为一了,创建一个对象的同时就发起了连接。

通过上述解释我们可以发现一个核心点:

Java 与 c中server在接受一个socket客户端的时候都会基于这个socket去创建一个新的socket描述,那我们为什么要这样做呢?为什么不直接让Java中的 ServerSocket创建的服务端套接字直接去处理客户端的请求呢?

其实这是为了

  • 实现高并发和可扩展 : 主线程可以拥有一个 ServerSocket 循环地、不停地接受新连接。每接受一个,就把它丢给一个新的线程(或线程池中的一个线程)去处理。这样,监听线程永远不会因为处理具体的数据读写而被阻塞,使得服务器可以同时为成百上千的客户端提供服务。
  • 进行状态隔离: 每个socket对象都包含了与之对应的客户端的状态信息,比如ip地址,端口号,输入输出流,这种设计将会话隔离开,互不干扰,代码逻辑清晰明了。
  • 清晰的面向对象设计:ServerSocket 类负责服务端级别的监听,Socket类负责数据的通信,符合单一职责原则。

并发性与扩展性

通过上面的描述我们知道了ServerSocket类可以进行accept阻塞等待新的客户端连接,每当新的客户端连接的时候会创建一个新的socket对象,里面包括客户端的ip,端口等信息。

但是如果服务端使用Server Socket的accept方法阻塞等待后只创建一个socket的话,那么后续其他客户端将无法连接到服务端,即便我们使用了while循环,但实际这个socket只保存了第一个客户的状态信息。

那么我们可以基于此处进行扩展,每当创建一个新的socket后,我们将这个socket交给子线程去处理,每个子线程实现与不同的客户端进行对接,从而避免主线程一直处理第一个客户端,其他客户端不能连接的情况。

  • 服务端主函数代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.mcc.socket;

import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8080);
while (true) {
Socket socket = ss.accept();
new Thread(new ServerReader(socket)).start();//start()启动一个子线程并且执行run任务方法
}
} catch (Exception e) {
System.out.println("发生错误: " + e.getMessage());
}
}
}

  • 子线程处理代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.mcc.socket;

import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;

public class ServerReader extends Thread{
private Socket socket;

public ServerReader(Socket socket) {
this.socket = socket;
}

public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);

while (true) {
String msg = dis.readUTF();
System.out.println("\n收到客户端消息:" + msg);
System.out.printf("客户端地址:%s:%d\n", socket.getInetAddress(), socket.getPort());
System.out.printf("----------------------------------------------");
}
} catch (Exception e) {
throw new RuntimeException(e);
}

}
}

  • 客户端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.mcc.socket;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

import static java.net.InetAddress.getLocalHost;

public class ClientDemo {
public static void main(String[] args) {
try {
Socket socket = new Socket(getLocalHost(), 8080);
OutputStream os = socket.getOutputStream();//得到字节输出流
DataOutputStream dos = new DataOutputStream(os);//创建数据输出流
Scanner sc = new Scanner(System.in);
while (true){
System.out.printf("请输入要发送的内容:");
String msg = sc.nextLine();
if (msg.equals("exit")){
break;
}
dos.writeUTF(msg);
}
dos.flush();
os.close();
socket.close();
} catch (IOException e) {
System.out.printf("客户端异常或服务端关闭连接:%s",e.getMessage());
}
}
}

通过上述代码,我们既然知道了可以进行创建子线程来处理不同客户端的请求,那么我们不妨进一步升级,采用可以线程复用的线程池技术,避免频繁的创建与销毁线程带来的性能损失

只需要增加线程池部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.mcc.socket;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ServerDemo {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8080);
ExecutorService pool = new ThreadPoolExecutor(
3, 10, 10L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy());
//核心线程数,最大线程数,临时线程多久不用关闭时间,工作队列,无法处理任务的中断策略
while (true) {
Socket socket = ss.accept();
pool.execute(new ServerReader(socket));
}
} catch (Exception e) {
System.out.println("发生错误: " + e.getMessage());
}
}
}

这样,我们就学到了如何利用多线程技术处理多客户端,以及基于线程池的线程复用技术。