本篇会加入个人的所谓鱼式疯言
❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言
而是理解过并总结出来通俗易懂的大白话,
小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.
🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!
引言
在这个数字化时代,信息的快速流通已成为我们日常生活的一部分。想象一下,当你发送一封电子邮件或浏览网页时,背后是一系列复杂的网络协议在默默工作,确保数据的准确传输。
今天,我们将聚焦于这些协议中的一个核心成员------传输控制协议(TCP)。TCP协议是互联网通信的基石,它通过确保数据的可靠传输,支撑着我们每天使用的无数网络服务。
目录
-
Tcp回显服务器的普通实现
-
Tcp 回显服务器的多线程实现
-
Tcp 回显服务器的线程池实现
一. Tcp 回显服务器的普通实现
1. Tcp 的特点
在上一篇 实现Udp 的回显服务器的文章中 , 我们提及Tcp 和 Udp 分别有4个主要的特点
- Tcp 是
可靠
传输, Udp是不可靠
传输。 - Tcp是面向 字节流, Udp是面向 数据报。
- Tcp 是
有连接
, Udp是无连接
。 - Tcp 是 全双工 , Udp也是 全双工 。
关于以上这些 特点 还有 回显服务器
的详解全部都收录到了 【网络编程通关之路】 Udp 基础回响服务器(Java实现)及你不知道知识原理详解 ! ! ! 这篇文章中 。
还没学习过或者忘记的小伙伴记得去看看哦。
2. Tcp使用的相关类
Tcp
与Udp
一样, 在进行网络通信的过程中,需要调用系统 Socket api
, 但在 Java标准库 中已经封装好了相关的类, 通过这几个类, 在 JVM
中来调用系统原生的 Socket api
来 操作网卡 , 进行网络通信。
关于Tcp 来实现回显
ServerSocket 类 :
-
实例化ServerSocket
对象时, 只用于服务器一端 , 并且进行端口号
连接 进程 的过程。 -
用于建立
服务器
与客户端
连接,并且返回Socket
对象。accept() 方法
鱼式疯言
从上面可以确定 Tcp的 有连接
的特点之一。
ServerSocket的构造方法 的连接是 端口号与进程的连接 , 相当于 "接好了电话线"
。
accept() 的连接是 服务器与客户端的连接 , 相当于 客户端那边给服务器 "打电话" , 而服务器这边就用 accept()
来接电话。完成连接。
Socket 类 :
-
用于客户端与服务器连接:
Socket 的构造方法, 在 实例化Socket 对象 时, 添加
IP地址
和端口号
, 就意味着建立了连接
。 -
操作 Socket对象 来获取各种
IP地址 和
输入和输出流对象
。获取IP地址
getInetAddress()
获取输入字节流方法
getInputStream()获取输出字节流方法
getOutputStream()
鱼式疯言
- 由于我们
Tcp 特点
之一就是 面向字节流 , 并且是全双工
。所以我们既可以获取到 输入字节流对象 ,也可以获取到 输出字节流对象 。
2.ServerSocket
类一般只适用于 服务器一端的端口号和进程的连接 , 而Socket
一般只适用于客户端一端
与 服务器 的连接, 但是都适用于 服务器和客户端 的 获取输入流与输出流 的操作。
3. 在 同一台主机 上,同一个协议 , 一个端口号
直接连接一个进程
。但是在不同协议中, 一个端口号 可以连接
不同协议
下的进程。
4. 无论是ServerSocket 类
还是Socket 类
, 都是属于java.net
下的包的类。
回显服务器的普通实现代码展示
<1>. 服务器实现
java
package echoTCP;
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.Executor;
import java.util.concurrent.Executors;
public class MyTcpEchoServer {
ServerSocket serverSocket = null;
// 进行连接
public MyTcpEchoServer(int port) {
try {
// 进行连接
serverSocket = new ServerSocket(port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void start() {
// 开始交互
System.out.println("服务器开始运行...");
while(true) {
// 使用线程池
Socket socket = null;
try {
socket = serverSocket.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 进行接收请求并响应
ProcessServerConnect(socket);
}
}
private void ProcessServerConnect(Socket socket) {
try(InputStream inputSTream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()
) {
while(true) {
System.out.printf("客户端上线 : [客户端ip = %s,
客户端端口号 = %d]\n"
,socket.getInetAddress(),
socket.getPort());
// 得到请求
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
// 得到请求
String request = scanner.nextLine();
// 计算响应
// 执行业务
String response = process(request);
// 返回响应
// 让客户端得到业务的执行结果
PrintWriter printWriter = new
PrintWriter(outputStream);
// 开始返回
printWriter.println(response);
// 进行刷新
printWriter.flush();
// 打印日志
System.out.printf("客户端下线 : [ip = %s ,
port = %d], 请求 = %s , 响应 = %s\n",
socket.getInetAddress(),
socket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.out.printf("任务执行结束: [%s,%d]\n",
socket.getInetAddress(),
socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) {
MyTcpEchoServer myTcpEchoServer = new MyTcpEchoServer(9090);
myTcpEchoServer.start();
}
}
服务器一端的实现的整体流程可分为以下几步:
- 创建
ServerSocket
对象 , 在构造方法中参数列表添加 端口号 , 建立端口号与进程的连接
。
java
// 进行连接
serverSocket = new ServerSocket(port);
- 调用
accept
方法让服务器与客户端进行,并返回一个Socket
对象。
java
Socket socket = null;
try {
socket = serverSocket.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
- 获取到输入流,利用
Scanner
包装输入流 得到请求 。
java
// 得到请求
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
// 得到请求
String request = scanner.nextLine();
计算响应
并把 输出流对象 包装到printWriter
输出到客户端
java
// 计算响应
// 执行业务
String response = process(request);
// 返回响应
// 让客户端得到业务的执行结果
PrintWriter printWriter = new
PrintWriter(outputStream);
// 开始返回
printWriter.println(response);
// 进行刷新
printWriter.flush();
- 打印日志 ,并关闭
Socket
对象
java
// 打印日志
System.out.printf("客户端下线 : [ip = %s ,
port = %d], 请求 = %s , 响应 = %s\n",
socket.getInetAddress(),
socket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.out.printf("任务执行结束: [%s,%d]\n",
socket.getInetAddress(),
socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
鱼式疯言
补充细节:
对于大部分
IO流
来说, 并不是 输入或输出一个/ 一段 就会加载到硬盘
, 而是会存放到一个叫 缓冲区的地方, 当数据积攒到一定的量
, 才会 加载过去 。所以我们为了每次只加载 一个 / 一段数据 , 可以调用
flush
这个方法 来将 缓冲区 的数据全部
刷新出去
。
java
// 进行刷新
printWriter.flush();
- 小编在这里利用了
Scanner
和printWriter
来包装输入和输出流。 但是小伙伴们也可以利用小编之前提及过的 IO流的操作方式 来进行哦, 效果是一样的 。
关于IO的流的操作文章, 可以多学习学习并且回去哦 💖 💖 💖 💖
- 对于上述 回显服务器 来说, 服务端一端
accept()
只有当服务器每次发来请求时 ,才会进行 连接, 否则进行阻塞等待
。
<2>. 客户端实现
java
package echoTCP;
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;
public class MyTcpEchoClient {
Socket clientSocket = null;
// 进行连接
public MyTcpEchoClient(String ip , int port) {
try {
// 进行连接
clientSocket = new Socket(ip, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void start() {
// 开始交互
System.out.println("客户端开始运行...");
// 发送请求并接受响应
ProcessServerConnect(clientSocket);
}
private void ProcessServerConnect(Socket socket) {
try(InputStream inputSTream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()
) {
while(true) {
Scanner in = new Scanner(System.in);
System.out.print("请输入你的需求-> ");
// 输入请求并发送
String request = in.nextLine();
PrintWriter printWriter = new
PrintWriter(outputStream);
printWriter.println(request);
// 进行刷新
printWriter.flush();
// 接收响应并输出
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
String response = scanner.nextLine();
System.out.println("response = "+ response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.out.printf("任务执行结束:
[%s,%d]\n",
socket.getInetAddress(),
socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
MyTcpEchoClient myTcpEchoClient = new
MyTcpEchoClient("127.0.0.1",9090);
myTcpEchoClient.start();
}
}
具体客户端一端的整体实现流程梳理:
- 实例化Socket 对象, 并把
IP地址
和端口号
作为Socket
构造方法的参数进行 客户端与服务端 的连接。
java
Socket clientSocket = null;
// 进行连接
public MyTcpEchoClient(String ip , int port) {
try {
// 进行连接
clientSocket = new Socket(ip, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
- 输入请求 并发送给
客户端
java
Scanner in = new Scanner(System.in);
System.out.print("请输入你的需求-> ");
// 输入请求并发送
String request = in.nextLine();
PrintWriter printWriter = new
PrintWriter(outputStream);
printWriter.println(request);
// 进行刷新
printWriter.flush();
- 接收服务器返回的 响应并输出
java
// 接收响应并输出
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
String response = scanner.nextLine();
System.out.println("response = "+ response);
- 宣布服务器执行结束 , 并 关闭Socket对象
java
finally {
System.out.printf("任务执行结束:
[%s,%d]\n",
socket.getInetAddress(),
socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
鱼式疯言
补充细节 :
- 对应上述代码中的
hasNext()
方法来说, 出现了换行
才会 break 结束 出去, 而当 服务器/ 客户端没有
发来数据时, 是会进入 阻塞等待 的, 不会break 直接跳出。
java
if(!scanner.hasNext()) {
break;
}
二. Tcp 回显服务器的多线程实现
1. 代码展示
服务器源码:
java
package echoTCP;
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.Executor;
import java.util.concurrent.Executors;
public class MyTcpEchoServer {
ServerSocket serverSocket = null;
// 进行连接
public MyTcpEchoServer(int port) {
try {
// 进行连接
serverSocket = new ServerSocket(port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void start() {
// 开始交互
System.out.println("服务器开始运行...");
// 使用多线程
while(true) {
Thread t = new Thread(()->{
Socket socket = null;
try {
socket = serverSocket.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 进行接收请求并响应
ProcessServerConnect(socket);
});
t.start();
}
}
private void ProcessServerConnect(Socket socket) {
try(InputStream inputSTream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()
) {
while(true) {
System.out.printf("客户端上线 : [客户端ip = %s,客户端端口号 = %d]\n",socket.getInetAddress(),socket.getPort());
// 得到请求
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
// 得到请求
String request = scanner.nextLine();
// 计算响应
// 执行业务
String response = process(request);
// 返回响应
// 让客户端得到业务的执行结果
PrintWriter printWriter = new PrintWriter(outputStream);
// 开始返回
printWriter.println(response);
// 进行刷新
printWriter.flush();
// 打印日志
System.out.printf("客户端下线 : [ip = %s , port = %d], 请求 = %s , 响应 = %s\n",
socket.getInetAddress(),socket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.out.printf("任务执行结束: [%s,%d]\n",socket.getInetAddress(),socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) {
MyTcpEchoServer myTcpEchoServer = new MyTcpEchoServer(9090);
myTcpEchoServer.start();
}
}
客户端源码 :
java
package echoTCP;
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;
public class MyTcpEchoClient {
Socket clientSocket = null;
// 进行连接
public MyTcpEchoClient(String ip , int port) {
try {
// 进行连接
clientSocket = new Socket(ip, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void start() {
// 开始交互
System.out.println("客户端开始运行...");
// 发送请求并接受响应
ProcessServerConnect(clientSocket);
}
private void ProcessServerConnect(Socket socket) {
try(InputStream inputSTream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()
) {
while(true) {
Scanner in = new Scanner(System.in);
System.out.print("请输入你的需求-> ");
// 输入请求并发送
String request = in.nextLine();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 进行刷新
printWriter.flush();
// 接收响应并输出
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
String response = scanner.nextLine();
System.out.println("response = "+ response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.out.printf("任务执行结束: [%s,%d]\n",socket.getInetAddress(),socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
MyTcpEchoClient myTcpEchoClient = new MyTcpEchoClient("127.0.0.1",9090);
myTcpEchoClient.start();
}
}
2. 原理剖析
对于多线程的回显服务器的实现, 最主要是解决 多个
客户端同时给 一个
服务器 发送请求 的问题。
对于服务器来说 外边套着一个循环 , 里面又加了 一层循环 , 这样的话就会导致 两层循环 的加入
当 第一个客户端进入内循环 时, 它的任务还没结束
, 第二个客户端就向服务器 发送请求 , 这时由于内部的还 一直循环, 服务器就无法进入内循环
, 只能让第二个客户端一直停留在缓冲区
。
这时我们就引入 多线程 的方式, 将每一个服务器都分布同时
执行循环, 这样就能保证不会让多个
服务器一直阻塞到缓冲区
。
关于代码的实现实现流程, 只是在普通回显服务器的基础上加了 一行多线程 的代码。 其他都是一样的, 小编在这里就 不赘述了 。
三. Tcp 回显服务器的线程池实现
1. 代码展示
服务器一端:
java
package echoTCP;
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.Executor;
import java.util.concurrent.Executors;
public class MyTcpEchoServer {
ServerSocket serverSocket = null;
// 进行连接
public MyTcpEchoServer(int port) {
try {
// 进行连接
serverSocket = new ServerSocket(port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void start() {
// 开始交互
System.out.println("服务器开始运行...");
while(true) {
// 使用线程池
Executor executor = Executors.newCachedThreadPool();
executor.execute(() -> {
Socket socket = null;
try {
socket = serverSocket.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 进行接收请求并响应
ProcessServerConnect(socket);
});
}
}
private void ProcessServerConnect(Socket socket) {
try(InputStream inputSTream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()
) {
while(true) {
System.out.printf("客户端上线 : [客户端ip = %s,客户端端口号 = %d]\n",socket.getInetAddress(),socket.getPort());
// 得到请求
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
// 得到请求
String request = scanner.nextLine();
// 计算响应
// 执行业务
String response = process(request);
// 返回响应
// 让客户端得到业务的执行结果
PrintWriter printWriter = new PrintWriter(outputStream);
// 开始返回
printWriter.println(response);
// 进行刷新
printWriter.flush();
// 打印日志
System.out.printf("客户端下线 : [ip = %s , port = %d], 请求 = %s , 响应 = %s\n",
socket.getInetAddress(),socket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.out.printf("任务执行结束: [%s,%d]\n",socket.getInetAddress(),socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) {
MyTcpEchoServer myTcpEchoServer = new MyTcpEchoServer(9090);
myTcpEchoServer.start();
}
}
客户端一端 :
java
package echoTCP;
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;
public class MyTcpEchoClient {
Socket clientSocket = null;
// 进行连接
public MyTcpEchoClient(String ip , int port) {
try {
// 进行连接
clientSocket = new Socket(ip, port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void start() {
// 开始交互
System.out.println("客户端开始运行...");
// 发送请求并接受响应
ProcessServerConnect(clientSocket);
}
private void ProcessServerConnect(Socket socket) {
try(InputStream inputSTream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()
) {
while(true) {
Scanner in = new Scanner(System.in);
System.out.print("请输入你的需求-> ");
// 输入请求并发送
String request = in.nextLine();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 进行刷新
printWriter.flush();
// 接收响应并输出
Scanner scanner = new Scanner(inputSTream);
if(!scanner.hasNext()) {
break;
}
String response = scanner.nextLine();
System.out.println("response = "+ response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
System.out.printf("任务执行结束: [%s,%d]\n",socket.getInetAddress(),socket.getPort());
try {
// 关闭 socket 流
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
MyTcpEchoClient myTcpEchoClient = new MyTcpEchoClient("127.0.0.1",9090);
myTcpEchoClient.start();
}
}
代码说明
关于线程池的代码实现: 主要还是在原来 普通 回显服务器的基础上, 加上了线程池的
newCachedThreadPool
对象 。
这个线程池对象在 任务繁忙 时, 最多可以创建出Integer.MAX_VALUES
个 非核心线程。
具体详情小伙伴们可以参考多线程的文章, 有重点讲解哦
非常适合 服务器开发使用 。
java
// 使用线程池
Executor executor = Executors.newCachedThreadPool();
executor.execute(() -> {
Socket socket = null;
try {
socket = serverSocket.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 进行接收请求并响应
ProcessServerConnect(socket);
});
鱼式疯言
如果是大量
的客户端, 我们可能会用到上面的 线程池
来使用。
如果是海量
的客户端使用, 就会使用 IO多路复用
(了解即可)。
总结
-
Tcp回显服务器的普通实现: 从Tcp 的四大特点: 有连接, 面向字节流, 可靠传输, 全双工 的四个特点展开介绍
Tcp
的两个主要网络通信的类:ServerSocket
和Socket
两个类 ,并在代码中充分的使用并结合理解流程。 -
Tcp 回显服务器的 多线程实现 : 利用多线程解决了一个服务器只能解决一个客户端的问题。
-
Tcp 回显服务器的线程池实现: 从另外一个线程池的角度更解决大量客户端的问题。
如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正
希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖