点进来你就是我的人了
博主主页:戳一戳,欢迎大佬指点!
人生格言:当你的才华撑不起你的野心的时候,你就应该静下心来学习!欢迎志同道合的朋友一起加油喔
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个嘿嘿
谢谢你这么帅气美丽还给我点赞!比个心
目录
一.Socket概述
Socket通信是有两种方式的:TCP和UDP
TCP与UDP区别
socket之send和recv原理剖析
二. TCP通信客户端Socket
三. TCP通信服务器端ServerSocket
四.基于TCP的Socket通信
五.UDP相关类DatagramPacket类和DatagramSocket类
数据包类DatagramPacket
发送数据包类DatagramSocket
InetAddress类(无构造方法)
六.基于UDP的Socket通信
七. TCP和UCP的缓冲区
1.TCP的缓冲区
2.UDP的缓冲区
Socket(套接字),是网络上两个程序之间实现数据交换的一端,它既可以发送请求,也可以接受请求,一个Socket由一个IP地址和一个端口号唯一确定,利用Socket能比较方便的实现两端(服务端和客户端)的网络通信。
在Java中,有专门的Socket类来处理用户请求和响应,学习使用Socket类方法,就可以实现两台机器之间通信。
TCP通信:客户端提供了java.net.Socket
类,服务器端提供了java.net.ServerSocket
类。
UDP通信:UDP通信不建立逻辑连接,使用DatagramPacket
类打包数据包,使用DatagramSocket
类发送数据包。
Socket通信模型如下图:
socket之send和recv原理剖析
当创建一个TCP socket对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存中的一片空间。
send原理剖析send发数据,必须得通过网卡发送数据,应用程序是无法直接通过网卡发送数据的,它需要调用操作系统接口,也就是说,应用程序把发送的数据先写入到发送缓冲区(内存中的一片空间),再由操作系统控制网卡把发送缓冲区的数据发送给服务端网卡。
recv原理剖析应用软件是无法直接通过网卡接收数据的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区(内存中的一片空间),应用程序再从接收缓存区获取客户端发送的数据。
Java中专门用来实现Socket客户端的类就叫Socket
,这个类实现了客户端套接字,用于向服务器发出连接请求等。
构造方法:
Socket(String host, int port)
:创建一个流套接字并将其连接到指定IP地址的指定端口号。
如果host为null,则相当于指定地址为回送地址。
127.x.x.x是本机的回送地址,即主机IP堆栈内部的IP地址,主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,协议软件立即返回之,不进行任何网络传输。
主要方法:
InputStream getInputStream()
:返回此套接字的输入流。
关闭生成的InputStream也将关闭相关的Socket。
OutputStream getOutputStream()
:返回此套接字的输出流。
关闭生成的OutputStream也将关闭相关的Socket。
void close()
:关闭此套接字
Java中专门用来建立Socket服务器的类叫ServerSocket
,这个类实现了服务器套接字,该对象等待通过网络的请求。
构造方法:
ServerSocket(int port)
:创建绑定到特定端口的服务器套接字。
主要方法:
Socket accept()
:监听并接受连接,返回一个新的Socket对象,用于和客户端通信,该方法会一直阻塞直到建立连接。
void close()
:关闭此套接字。
步骤分析:
getOutputStream()
方法获取OutputStream
对象,并使用write()
方法将数据写入到发送缓冲区。随后,通过调用flush()
方法确保数据已被发送出去getInputStream()
方法获取与该socket关联的InputStream
实例,然后使用read()
方法从接收缓冲区中读取数据。服务器端:
public class TcpEchoServer {
// serverSocket 就是外场拉客的小哥
// clientSocket 就是内场服务的小姐姐.
// serverSocket 只有一个. clientSocket 会给每个客户端都分配一个~
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();
//服务器的主线程(main 线程)负责运行 while 循环,用于接收客户端的连接请求。
// 与此同时,针对每个接收到的连接请求,都会创建一个新线程处理与该客户端的数据通信。这些新线程与主线程是并发执行的。
//由于主线程和新创建的线程并发执行,服务器可以在处理一个客户端连接的同时,继续接收其他客户端的连接请求。
// 这使得服务器可以并发处理多个客户端连接,提高了服务器的处理能力。
// 创建新的线程, 用新线程来调用 processConnection
// Thread t = new Thread(() -> {
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.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 (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n
// 不是不能做, 而是代码比较麻烦.
// 为了简单, 把字节流包包装成了更方便的字符流~~
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 读取一段字符串.
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行的.
printWriter.println(response);
//数据此时还在缓存区,使用 flush() 方法,可以确保数据立即发送
printWriter.flush();
//clientSocket.getInetAddress().toString() 返回客户端的 IP 地址,clientSocket.getPort() 返回客户端的端口号,
// request 是客户端发送的请求,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 秒)时,线程池会回收这个空闲线程。
好处:线程池可以复用已经创建的线程,避免了频繁地创建和销毁线程所带来的性能开销。当有新任务到来时,线程池会优先使用空闲的线程,从而提高系统资源的利用率。
客户端:
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. 把读取的内容构造成请求, 发送给服务器.
//在 Java 中,可以通过 PrintWriter 对象的 println() 方法发送带有换行符的数据包。
// 该方法会将指定的字符串添加一个换行符,并将其发送到输出流中。
printWriter.println(request);
//数据此时还在缓存区,使用 flush() 方法,可以确保数据立即发送
printWriter.flush();
// 3. 从服务器读取响应内容
//next() 方法会读取下一个标记,而不是一行数据。标记通常是以空格、制表符或换行符为分隔符的单词或符号。
// 因此,在读取数据时,如果数据包中只有一个标记,则可以使用 next() 方法读取该标记。
String response = scannerFromSocket.next();
// 4. 把响应结果显示到控制台上.
// request 是客户端发送的请求,response 是服务器响应的数据。
// 通过格式化输出的方式将这些信息打印出来,方便程序员进行调试和查看。
System.out.printf("req: %s; resp: %s\n", request, response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
PrintWriter printWriter = new PrintWriter(outputStream);
outputStream 是一个字节输出流,通过 PrintWriter 对象的构造方法将其包装成字符输出流,以便能够方便地写入字符数据。
Scanner scannerFromSocket = new Scanner(inputStream);
scannerFromSocket 对象是将输入流对象包装成一个 Scanner 对象,以便能够方便地读取输入流中的数据。
Scanner 对象会自动解析和分隔输入流中的数据,并将其转换为相应的数据类型或字符串。
DatagramPacket(byte[] buf, int length)
:构造DatagramPacket,用来接收长度为length的数据包。DatagramPacket(byte[] buf, int length, InetAddress address, int port)
:构造数据报包,用来将长度为length的包发送到指定主机上的指定端口号。public int getLength()
:获得发送端实际发送的字节数或接收端世界接收的字节数public int getPort()
:获得发送端或接收端端口号DatagramSocket()
:构造数据报套接字并将其绑定到本地主机上任何可用的端口。DatagramSocket(int port)
:创建数据包套接字并将其绑定到本地主机上指定端口。public void send(DatagramPacket p)
:从此套接字发送数据报包public void receive(DatagramPacket p)
:从此套接字接收数据报包public void close()
:关闭此数据报套接字public static InetAddress getLocalHost()
:返回本地主机public static InetAddress getByName()
:在给定主机名的情况下确定主机的 IP 地址。public String getHostName()
: 获取此 IP 地址的主机名。public String getHostAddress()
:返回 IP 地址字符串(以文本表现形式)步骤分析
服务器端:
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();
}
}
UdpEchoServer(UDP 服务器):
a. 首先,创建一个
DatagramSocket
对象,并绑定到指定的端口。这个端口用于服务器与客户端之间的通信。b. 在服务器的主循环中,首先创建一个空的
DatagramPacket
对象,用于接收客户端发来的请求数据。c. 调用
socket.receive(requestPacket)
方法接收客户端发来的数据包。d. 将收到的数据包中的数据转换成字符串形式,并调用
process()
方法生成响应。在这个例子中,响应就是原请求。e. 创建一个新的
DatagramPacket
对象,包含响应数据和客户端的地址信息。f. 使用
socket.send(responsePacket)
方法将响应数据包发送回客户端。g. 打印请求和响应信息。
客户端:
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();
}
}
UdpEchoClient(UDP 客户端):
a. 创建一个
DatagramSocket
对象,不需要显式地关联端口,系统会自动分配一个空闲的端口。b. 在客户端的主循环中,从控制台读取用户输入的字符串作为请求。
c. 创建一个
DatagramPacket
对象,包含请求数据、服务器的 IP 地址和端口信息。d. 调用
socket.send(requestPacket)
方法将请求数据包发送给服务器。e. 创建一个空的
DatagramPacket
对象,用于接收服务器返回的响应数据。f. 调用
socket.receive(responsePacket)
方法接收服务器发来的响应数据包。g. 将收到的响应数据包中的数据转换成字符串形式,并打印请求和响应信息。
总结:这两段代码实现了一个简单的 UDP 回显服务器和客户端。客户端将用户输入的请求数据通过 UDP 协议发送给服务器,服务器接收到请求后原样返回响应,客户端接收响应并打印信息。整个过程使用无连接的 UDP 协议进行通信。
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用write时,数据会先写入发送缓冲区中;
如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适
的时机发送出去;
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面,TCP的另一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既
可以读数据,也可以写数据。这个概念叫做 全双工
UDP只有接收缓冲区,没有发送缓冲区:
UDP没有真正意义上的 发送缓冲区。发送的数据会直接交给内核,由内核将数据传给网络层协议
进行后续的传输动作;
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一
致;如果缓冲区满了,再到达的UDP数据就会被丢弃;