赞
踩
目录
java.net.Socket
类,服务器端提供了
java.net.ServerSocket
类。
DatagramPacket
类打包数据包,使用
DatagramSocket
类发送数据包。
类似于两辆车在高速公路行驶,中间是一个隔离带,车辆可以既可以从这个方向走,也可以沿着那条方向上走,支持双向通信。
Socket通信模型如下图:
Socket的发送和接受原理剖析
当创建一个TCP socket对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存中的一片空地。
发送原理
send发送数据,必须得通过网卡发送数据,应用程序是无法通过网卡发送数据的,它需要调用操作系统接口,也就是说,应用程序把发送的数据先写入发送缓冲区(内存中的一块空地),再由操作系统控制网卡把发送缓冲区的数据发送给服务器网卡。
接收原理
应用程序无法直接从网卡接收数据的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区,应用程序再从接收缓冲区获取客户端发送的数据。
DatagramSocket API
DatagramSocket API是UDP Socket,用于发送和接收UDP数据报
DatagramSocket 构造方法
方法 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到主机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到主机指定的端口(一般用于服务器) |
这里简单介绍下构造方法
不带参数的构造方法一般用于客户端,举一个简单的例子,比如去食堂吃饭,到一个店铺去点餐,在老板还没弄好之前我会随机挑选一个位置,如果这个座位被占用了,我会选下一个座位,所以客户端一般不带有端口号;带参数的构造方法一般用于服务器,同理去食堂吃饭,我和老板提前商量会去他那吃饭,它需要给我一个详细的地址,这个地址就相当于端口号,以至于我到了不会去错地方,所以服务器一般带有端口号。
DatagramSocket 方法
方法 | 方法说明 |
void recieve(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p)
| 从此套接字发送数据报(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
DatagramPacket API
方法 | 方法说明 |
DatagramPacket(byte[]
buf, int length)
|
构造一个
DatagramPacket
以用来接收数据报,
接收
的数据保存在
字节数组(第一个参数
buf
)中,接收指定长度(第二个参数length)
|
DatagramPacket(byte[]
buf, int offset, int length,
SocketAddress address)
|
构造一个
DatagramPacket
以用来发送数据报,
发送
的数据为字节
数组(第一个参数
buf
)中,从
0
到指定长度(第二个参数
length
)。
address
指定目的主机的
IP
和端口号
|
DatagramPacket 方法
方法 | 方法说明 |
InetAddress getAddress()
|
从接收的数据报中,获取发送端主机
IP
地址;或从发送的数据报中,获取
接收端主机
IP
地址
|
int getPort()
|
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获
取接收端主机端口号
|
byte[] getData()
|
获取数据报中的数据
|
方法 | 方法说明 |
InetSocketAddress(InetAddress addr, int port)
|
创建一个
Socket
地址,包含
IP
地址和端口号
|
2.1 实例
使用UDP版的回响服务器进行通信
服务端
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.SocketException;
-
-
- public class UdpEchoServer {
- //需要先定义一个socket对象
- //通过网络通信,必须要使用socket对象
- private DatagramSocket socket = null;
-
- //为什么需要抛一个异常呢? 因为绑定一个端口不一定能成功
- //如果某个端口已经被别的进程占用了,此时这里绑定操作就会出错
- //同一个主机上,一个端口,同一时刻只能被一个进程绑定
- public UdpEchoServer(int port) throws SocketException {
- //构造socket的同时,指定要关联/绑定的端口
- socket = new DatagramSocket(port);
- }
-
- //启动服务器的主逻辑
- public void start() throws IOException {
- System.out.println("服务器启动!");
- while(true) {
- //每次循环,需要做三件事情
- //1、读取请求并解析
- //构造一个"空饭盒"
- DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
- //类似于食堂阿姨给饭盒里盛饭(饭从网卡上来)
- socket.receive(requestPacket);
- //为了方便处理这个请求,把数据包转成String
- String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
- //2、根据请求计算响应(此处省略这个步骤)
- String response = process(request);
- //3、把响应结果写回到客户端
- //根据response字符串,构造一个DatagramPacket
- //和请求packet不同,此处构造响应的时候,需要指定这个包要发给谁
- DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
- //requestPacket是从客户端这里收来的,getSocketAddress就会得到客户端的ip和端口
- requestPacket.getSocketAddress());
- socket.send(responsePacket);
- System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
- requestPacket.getPort(), request, response);
- }
- }
-
- //这个方法希望是根据请求计算响应
- //这里写的是一个回响程序,请求是啥,响应就是啥
- //如果后续写个别的服务器,不需要回响,而是有具体的业务了,就可以修改process方法
- //根据需要来重新构造响应
- public String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
- udpEchoServer.start();
- }
-
- }

这里讲解下这部分
因为receive方法是Java中的输出型参数,类似于这里我先使用DatagramPacket构造一个空饭盒,receive里面的参数就相当于食堂阿姨接收到了这个空饭盒,再给你这个空饭盒盛菜,也可以这么说,DatagramSocket相当于外卖员,DatagramPacket相当于外卖。
客户端
- import java.io.IOException;
- import java.net.*;
- import java.util.Scanner;
-
- public class UdpEchoClient {
- private DatagramSocket socket = null;
- private String serverIP;
- private int serverPort;
-
- //客户端启动,需要知道服务器在哪
- public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
- //对于客户端来说,不需要显示关联端口
- //不代表没有端口,而是系统自动分配了个空闲的端口
- socket = new DatagramSocket();
- this.serverIP = serverIP;
- this.serverPort = serverPort;
- }
-
- public void start() throws IOException {
- //通过这个客户端可以多次和服务器进行交互
- Scanner scanner = new Scanner(System.in);
- while (true) {
- //1、先从控制台读取一个字符串过来
- System.out.print("-> ");
- String request = scanner.next();
- //2、把字符串构造成UDP packet,并进行发送
- DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
- InetAddress.getByName(serverIP), serverPort);
- socket.send(requestPacket);
- //3、客户端尝试读取服务器返回的响应
- DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
- socket.receive(responsePacket);
- //4、把响应数据转换成String显示出来
- String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
- System.out.printf("req: %s, resp: %s\n", request, response);
- }
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
- udpEchoClient.start();
- }
- }

程序整个流程介绍:
1、服务器先启动,创建DatagramSocket对象,监听端口,用于接收
2、服务器端创建DatagramPacket对象,打包用于接收的数据包
3、服务器阻塞等待接收
4、客户端启动,创建DatagramSocket对象,监听端口,用于接收
5、客户端创建DatagramPacket对象,打包用于发送的数据包
6、客户端发送数据,服务端接收
7、服务端接收数据后,创建DatagramPacket对象,打包用于发送的数据包,发送数据
8、客户端创建DatagramPacket对象,打包用于接收的数据包,阻塞等待接收
9、客户端接收服务端数据,断开连接,释放资源
方法 | 方法说明 |
ServerSocket(int port)
|
创建一个服务端流套接字
Socket
,并绑定到指定端口
|
ServerSocket方法
方法 | 方法说明 |
Socket accept()
|
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端
Socket 对象,并基于该Socket
建立与客户端的连接,否则阻塞等待
|
void close()
|
关闭此套接字
|
方法 | 方法说明 |
Socket(String host, int port)
|
创建一个客户端流套接字
Socket
,并与对应
IP
的主机上,对应端口的
进程建立连接
|
Socket方法:
方法 | 方法说明 |
InetAddress getInetAddress()
|
返回套接字所连接的地址
|
InputStream getInputStream()
|
返回此套接字的输入流
|
OutputStream getOutputStream()
|
返回此套接字的输出流
|
3.1 实例
服务器端
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.io.PrintWriter;
- import java.net.ServerSocket;
- import java.net.Socket;
- import java.util.Scanner;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class TcpEchoServer {
- private ServerSocket serverSocket = null;
-
- public TcpEchoServer(int port) throws IOException {
- serverSocket = new ServerSocket(port);
- }
-
- public void start() throws IOException {
- ExecutorService executorService = Executors.newCachedThreadPool();
- System.out.println("启动服务器!");
- while (true) {
- Socket clientSocket = serverSocket.accept();
- //如果直接调用,该方法会影响到这个循环的二次执行,导致accept不及时了
- //创建新的线程,用新线程来调用processConnection
- //每次来一个新的客户端都搞一个新的线程即可
- // Thread thread = new Thread(() -> {
- // try {
- // processConnection(clientSocket);
- // } catch (IOException e) {
- // e.printStackTrace();
- // }
- // });
- // thread.start();
- executorService.submit(new Runnable() {
- @Override
- public void run() {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- });
- }
- }
-
- //通过这个方法来处理一个连接
- //读取请求
- //根据请求计算响应
- //把响应返回给客户端
- private void processConnection(Socket clientSocket) throws IOException {
- System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- //try()这种写法,()里面允许写多个流对象,使用;来分割
- try (InputStream inputStream = clientSocket.getInputStream();
- OutputStream outputStream = clientSocket.getOutputStream()){
- Scanner scanner = new Scanner(inputStream);
- PrintWriter printWriter = new PrintWriter(outputStream);
- while (true) {
- //1、读取请求
- if(!scanner.hasNext()) {
- //读取的流到结尾了(对端关闭了/客户端关闭连接)
- System.out.printf("[%s:%d] 客户端下线\n!", clientSocket.getInetAddress().toString(),
- clientSocket.getPort());
- break;
- }
- //直接使用scanner读取一段字符串
- //next操作读出的结果是不带换行的
- String request = scanner.next();
- //2、根据请求计算响应
- String response = process(request);
- //3、将响应写回给客户端,不要忘了响应里面也是要带换行的
- //返回响应要把换行加回来,方便客户端那边区分从哪里到哪里是一个完整响应
- printWriter.println(response);
- System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),
- clientSocket.getPort(), request, response);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- clientSocket.close();
- }
-
- }
-
- private String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
- tcpEchoServer.start();
- }
- }

在服务器端我们使用了try-with-resources语句,确保在代码块执行完毕后自动关闭资源,无论代码执行过程中是否发生异常。
当程序执行到try语句块结束时,如果resource实现了AutoCloseable或Closeable接口,那么close()方法将被自动调用。同时在服务器端引入了多线程的写法,保证服务器能连接多个客户端,同时与多个客户端保持通信,具体实现逻辑如下:
服务器的主线程(main 线程)负责运行 while 循环,用于接收客户端的连接请求。
与此同时,针对每个接收到的连接请求,都会创建一个新线程处理与该客户端的数据通信。这些新线程与主线程是并发执行的。由于主线程和新创建的线程并发执行,服务器可以在处理一个客户端连接的同时,继续接收其他客户端的连接请求。 这使得服务器可以并发处理多个客户端连接,提高了服务器的处理能力。当然我们也可以引入线程池来优化这段代码:
线程池中的线程数量是动态调整的。
当有新任务提交时,如果线程池中有空闲线程,那么会复用空闲线程来执行新任务;如果没有空闲线程,则会创建一个新线程来执行新任务。
当线程池中的线程空闲时间超过一定时间(默认为 60 秒)时,线程池会回收这个空闲线程。
好处:线程池可以复用已经创建的线程,避免了频繁地创建和销毁线程所带来的性能开销。当有新任务到来时,线程池会优先使用空闲的线程,从而提高系统资源的利用率。
客户端
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.io.PrintWriter;
- import java.net.Socket;
- import java.util.Scanner;
-
- public class TcpEchoClient {
- private Socket socket = null;
-
- public TcpEchoClient(String serverIp, int port) throws IOException {
- //这个操作就相当于客户端和服务器建立tcp连接
- //这里连接连上了,服务器的accept就会返回
- socket = new Socket(serverIp, port);
- }
-
- public void start() {
- Scanner scanner = new Scanner(System.in);
- try (InputStream inputStream = socket.getInputStream();
- OutputStream outputStream = socket.getOutputStream()){
- //outputStream是一个字节输出流,通过printWriter对象的构造方法将其包装成字符输出流,以便能够更方便地写入字符数据
- PrintWriter printWriter = new PrintWriter(outputStream);
- //scannerFromSocket对象是将输入流对象包装成一个scanner对象,以便能够方便地读取输入流中的数据
- //scanner对象会自动解析和分隔输入流中的数据,并将其转换为相应的数据类型或字符串
- Scanner scannerFromSocket = new Scanner(inputStream);
- while (true) {
- //1、从键盘上读取用户输入的内容
- System.out.print("->");
- String request = scanner.next();
- //2、把读取的内容构造成请求发送给服务器
- //注意,这里的发送是带有换行的
- printWriter.println(request);
- printWriter.flush();
- //3、从服务器读取响应内容
- String response = scannerFromSocket.next();
- //4、把响应的结果显示到控制台上
- System.out.printf("req: %s; resp: %s\n", request, response);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
- tcpEchoClient.start();
- }
- }

PrintWriter printWriter = new PrintWriter(outputStream);
outputStream 是一个字节输出流,通过 PrintWriter 对象的构造方法将其包装成字符输出流,以便能够方便地写入字符数据。
Scanner scannerFromSocket = new Scanner(inputStream);
scannerFromSocket 对象是将输入流对象包装成一个 Scanner 对象,以便能够方便地读取输入流中的数据。
Scanner 对象会自动解析和分隔输入流中的数据,并将其转换为相应的数据类型或字符串。
程序整个流程介绍:
1、服务端先启动,创建ServerSocket对象,等待连接。
2、客户端启动,创建Socket对象,请求连接。
3、服务器端接收请求,调用accept方法,并返回一个Socket对象,连接成功
4、客户端的Socket对象通过调用
getOutputStream()
方法获取OutputStream
对象,并使用write()
方法将数据写入到发送缓冲区。随后,通过调用flush()
方法确保数据已被发送出去5、服务器端Socket对象通过调用
getInputStream()
方法获取与该socket关联的InputStream
实例,然后使用read()
方法从接收缓冲区中读取数据6、客户端释放资源,断开连接。
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用write时,数据会先写入发送缓冲区中;
如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适
的时机发送出去;
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面,TCP的另一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既
可以读数据,也可以写数据。这个概念叫做全双工。
UDP只有接收缓冲区,没有发送缓冲区;
UDP没有真正意义上的发送缓冲区。发送的数据会直接交给内核,由内核将数据传给网络层协议
进行后续的传输动作;
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一
致;如果缓冲区满了,再到达的UDP数据就会被丢弃;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。