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 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(); } } 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()); } } }
|
这样,我们就学到了如何利用多线程技术处理多客户端,以及基于线程池的线程复用技术。