多线程与socket
以下摘要由GPT-4o生成:
JAVA中的Socket编程主要分为TCP和UDP两种类型。TCP采用ServerSocket监听特定端口并接受客户端连接,创建Socket对象进行通信;而UDP则使用DatagramSocket和DatagramPacket发送接收独立数据包。与C语言网络编程相比,Java的设计允许高并发和可扩展性,主线程通过循环不断接受新连接,并将每个连接交给新的子线程处理,从而避免阻塞。此外,这种做法也实现了状态隔离,使得每个Socket对象维护其客户端的信息,符合面向对象设计原则。为了提高性能,建议采用线程池技术,以减少频繁创建和销毁线程所带来的开销。这些措施共同提升了服务器同时服务多个客户端的能力。
java 中的 socket
先了解 Java 中的 net包中的主要方法
- TCP 相关 (面向连接):
- ServerSocket: 用于服务器端。它的核心职责是在服务器上监听一个特定的端口,等待客户端的连接请求。
- Socket: 用于客户端或服务器端。它代表了一个网络连接的两端中的一端。一旦服务器通过 ServerSocket 接受了一个连接,就会得到一个 Socket 对象,该对象代表了与那个特定客户端的专用通信通道。客户端为了发起连接,也会创建一个 Socket 对象。
- 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 | package com.mcc.socket; |
- 子线程处理代码:
1 | package com.mcc.socket; |
- 客户端代码:
1 | package com.mcc.socket; |
通过上述代码,我们既然知道了可以进行创建子线程来处理不同客户端的请求,那么我们不妨进一步升级,采用可以线程复用的线程池技术,避免频繁的创建与销毁线程带来的性能损失
只需要增加线程池部分代码:
1 | package com.mcc.socket; |
这样,我们就学到了如何利用多线程技术处理多客户端,以及基于线程池的线程复用技术。
