编辑
阿华代码,不是逆风,就是我疯
你们的点赞收藏是我前进最大的动力!!
希望本文内容能够帮助到你!!
目录
本文代码建议敲打至少3遍
一:TCP的API
1:SevereScoket类
(1)构造方法
编辑
(2)方法
编辑
注:accept可以接收多个客户端的请求连接,有阻塞功能
编辑
2:Socket类
Socket是客户端Socket,或者服务端收到客户端accept的请求后返回的服务端Socket,不管是客户端还是服务端,都是建立连接以后,保存对端的信息,以及用来与对方接收和发送数据的
(1)构造方法
编辑
(2)方法
编辑
二:基本代码实现
☆注:此处代码非完整版本,是一个最基本的框架,代码本身还有三个很重要的问题,需要解决,完整代码文章最后有上传
1:服务端
(1)有注释版
package InternetTcp; 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; /** * Created with IntelliJ IDEA. * Description: * User: Hua YY * Date: 2024-10-11 * Time: 17:01 */ public class TcpEchoServer { //不同于Udp的DatagramSocket文件,ServerSocket文件负责揽活 private ServerSocket serverSocket = null; public TcpEchoServer(int port) throws IOException { //1:建立socket文件,并构造 serverSocket = new ServerSocket(port);//导包抛异常 } public void start() throws IOException { System.out.println("服务器启动"); while(true){ //2:再建立一个Socket文件来进行连接通信(负责接待客户端),可阻塞 Socket clientSocket = serverSocket.accept(); //3:写一个方法来处理这次连接,包括了收到请求和发出响应 processConnection(clientSocket); } } private void processConnection(Socket clientSocket) throws IOException { //4:可以通过Socket文件获得客户端的ip和端口号 System.out.printf("[%s , %d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort()); //5:循环读取请求和返回响应 //Tcp是面向字节流,单位为字节,Socket本质就是文件,所以可以进行流操作 try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ //6:while循环不断读取此次连接的请求并返回响应 while(true){ //7:读取操作 /* byte[] buffer = new byte[1024]; inputStream.read(buffer);//把读取到的请求放到数组里 因为一会还要把这个请求转化为字符串,我们还有一个更简单拿到方法 */ //7:不要去放System.in输入,用(Socket对象)inputStream来帮助Scanner进行构造 // Scanner不仅可以从操作系统中读,也可以从文件,网络中读,Scanner放在while循环外也可以 Scanner scanner = new Scanner(inputStream); if(!scanner.hasNext()){ System.out.printf("[%s , %d],客户端断开连接\n",clientSocket.getInetAddress(),clientSocket.getPort()); break; } //8:根据请求计算响应 String request = scanner.next(); String response = process(request); //9:把响应返回给客户端(封装一下),进行写入 /* 直接用write方法写回响应,不方便添加换行符\n,因为客户端在读取响应的时候使用next,结束标志为\n、空格、tab outputStream.write(response.getBytes(),0,response.getBytes().length); */ PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); System.out.printf("[%s,%d] , request: %s , response : %s\n " ,clientSocket.getInetAddress(),clientSocket.getPort(),request,response); } } catch (IOException e) { throw new RuntimeException(e); } } public String process(String response){ return response; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } }
(2)无注释版
package repeat2; 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; /** * Created with IntelliJ IDEA. * Description: * User: Hua YY * Date: 2024-10-12 * Time: 9:57 */ public class TcpEchoServer { //socket文件来读写 private ServerSocket serverSocket = null;//拉客 public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动\n"); while(true){ //一次连接 Socket socketClient = serverSocket.accept();//拉完客了给接待 System.out.printf("[%s %d]客户端已上线\n",socketClient.getInetAddress(),socketClient.getPort()); processConnection(socketClient); } } public void processConnection(Socket socketClient){ //面向字节流 try(InputStream inputStream = socketClient.getInputStream(); OutputStream outputStream = socketClient.getOutputStream()){ while(true){ /* byte[] buffer = new byte[1024]; inputStream.read(buffer);//数组再转字符串 */ //接收请求 Scanner scanner = new Scanner(inputStream); if(!scanner.hasNext()){ System.out.printf("[%s %d]客户端断开连接\n", socketClient.getInetAddress(), socketClient.getPort()); break; } String request = scanner.next(); String response = process(request); //怎么发送呢?write不建议,封装outStream PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); System.out.printf("[%s %d] , req: %s resp: %s\n",socketClient.getInetAddress(),socketClient.getPort(),request,response); } } catch (IOException e) { throw new RuntimeException(e); } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9091); server.start(); } }
2:客户端
(1)有注释版
package InternetTcp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; /** * Created with IntelliJ IDEA. * Description: * User: Hua YY * Date: 2024-10-11 * Time: 17:01 */ public class TcpEchoClient { //1:创建一个Scoket对象 private Socket socket = null; //2:构造方法因为 TCP是有连接的 所以Scoket对象中包含服务器的IP和端口,实例化对象就会与服务器创立连接,服务器中的accept就会进行呼应 public TcpEchoClient(String serverIp , int serverPort) throws IOException { socket = new Socket(serverIp , serverPort); } public void start(){ System.out.println("客户端启动"); //一:获取控制台输入 //3:基于socket文件创建输入输出流,并实例化两个扫描器 try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){ Scanner scannerConsole = new Scanner(System.in);//控制台扫描器 Scanner scannerNetWork = new Scanner(inputStream); //5:调用PrintWriter中的write方法发送请求,用outputStream帮助构造 PrintWriter writer = new PrintWriter(outputStream); while(true){ System.out.print("->"); if (!scannerConsole.hasNext()){ break; } //二:构造请求,并发送 String request = scannerConsole.next(); writer.println(request);//呼应server中的获取响应scanner.next()//问题 //三:接收响应 String response = scannerNetWork.next(); //四:在显示器上进行打印 System.out.println(response); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } }
(2)无注释版
package repeat2; import jdk.internal.util.xml.impl.Input; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; /** * Created with IntelliJ IDEA. * Description: * User: Hua YY * Date: 2024-10-12 * Time: 9:57 */ public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp , int serverport){ socket = new Socket(); } public void start(){ System.out.println("客户端启动"); //从读取控制台的请求 //构造请求并发送 //接收响应 //把相应打印显示出来 try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){ Scanner scannerConsole = new Scanner(System.in); Scanner scannerNetWork = new Scanner(inputStream); PrintWriter writer = new PrintWriter(outputStream); while(true){ System.out.print("->"); String request = scannerConsole.next(); //怎么发送呢?outputStream(带封装) writer.println(request); //怎么接收响应? String response = scannerNetWork.next(); System.out.println(response); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { InternetTcp.TcpEchoClient client = new InternetTcp.TcpEchoClient("127.0.0.1",9091); client.start(); } }
三:细节问题
优化点①
(读/接受request)用Scanner替代数组转化为字符串,因为有文件流操作,用inputStream来帮助Scanner进行构造
编辑
优化点②
直接用outputStream的write方法不方便写换行符\n,所以给outputStream“封装一下”,用outputStream帮助PrintWriter进行构造,再使用println方法实现自动换行
编辑
构造方法
编辑
四: 过程梳理
编辑
编辑
编辑
编辑
五:PrintWriter缓冲区问题
1:问题引入
客户端发出请求没有收到响应
编辑
编辑
2:解决方式
引入PrintWriter中的flush()方法
之所以会出现上述问题是因为,PrintWriter的内置缓存区在发力,因为文件IO是比较低效的操作,所以操作系统会进行优化,尽可能的让这种操作少一点,就引入了缓存区(内存),把要写入网卡的数据放到内存缓冲区中,等攒一波大的,在统一发送;
换个说法就是如果发送的数据太少,数据就会先滞留在内存缓冲区中,没有被真正的发送出去。
编辑
编辑
六:Socket文件释放问题
1:问题引入
编辑
因为服务器每连接一个客户端都要建立一个Socket文件(名字叫clientSocket),这个文件是会占用文件描述符表的,连接的客户端数量多的话,只建立文件不释放文件的话就会造成——文件描述符表被占满
2:解决方式
每次服务器执行完客户端的请求后(即processConnection方法执行完毕)就释放掉Socket文件
编辑
七:多个客户端连接问题
1:问题引入
(1)如何运行多个同一程序
创建两个客户端,让服务器同时对两个服务端进行服务,最后再点运行就会出现两个Client了
编辑
编辑
编辑
(2)实际效果
编辑
编辑
编辑
本质原因:
accept使用了一次while循环,processClient方法中又嵌套了一层while循环
导致在服务器在处理客户端A的请求时,一直在processClient方法中出不来,就执行不了第二次客户端B的accept
(内核中:客户端B和服务器已经建立了TCP连接,但是用户态应用程序这里无法处理到,也就是拿不到)
(理解成:别人给你打电话,电话响了,但是你不接)
重点:虽然处理不了客户端B的数据请求,但是B的数据请求会暂时放在Socket缓冲区中(缓存区不能无限放数据,有大小限制的,所以可能会存在数据丢失的情况),等A端下线后,B端的请求会被瞬间处理掉
2:解决方式
引入多线程——把嵌套的两个while循环给拆分开来
编辑
编辑
编辑
八:线程池优化多线程问题
1:问题引入
上述八中引入多线程解决了多个客户端的上线问题,但是频繁的创建和销毁进程,会带来很大的资源消耗问题,我们给出了方案——引入线程池
2:解决方式
编辑
3:其他方案(了解即可)
(1)协程
轻量级线程,本质上还是一个线程,用户态可以通过手动调度的方式让这个线程“并发”的做多个任务
(2)IO多路复用
系统内核级别的机制,本质上是让一个线程同时去负责处理多个Socket(这虽然有多个socket数据,但是同一时刻活跃的socket只是少数,大部分socket都是在等)在Java中也有对应的封装了的API
九:写代码易错的地方
1:服务器
编辑
编辑
编辑
编辑
2:客户端
编辑
编辑
十:完整代码
服务器
package InternetTcp; 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; /** * Created with IntelliJ IDEA. * Description: * User: Hua YY * Date: 2024-10-11 * Time: 17:01 */ public class TcpEchoServer { //不同于Udp的DatagramSocket文件,ServerSocket文件负责揽活 private ServerSocket serverSocket = null; public TcpEchoServer(int port) throws IOException { //1:建立socket文件,并构造 serverSocket = new ServerSocket(port);//导包抛异常 } public void start() throws IOException { System.out.println("服务器启动"); while(true){ //2:再建立一个Socket文件来进行连接通信(负责接待客户端),可阻塞 Socket clientSocket = serverSocket.accept(); //3:写一个方法来处理这次连接,包括了收到请求和发出响应 ExecutorService pool = Executors.newCachedThreadPool(); pool.submit(new Runnable(){ @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } } }); /* Thread thread = new Thread(()->{ try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } }); thread.start(); */ } } private void processConnection(Socket clientSocket) throws IOException { //4:可以通过Socket文件获得客户端的ip和端口号 System.out.printf("[%s , %d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort()); //5:循环读取请求和返回响应 //Tcp是面向字节流,单位为字节,Socket本质就是文件,所以可以进行流操作 try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ //6:while循环不断读取此次连接的请求并返回响应 while(true){ //7:读取操作 /* byte[] buffer = new byte[1024]; inputStream.read(buffer);//把读取到的请求放到数组里 因为一会还要把这个请求转化为字符串,我们还有一个更简单拿到方法 */ //7:不要去放System.in输入,用(Socket对象)inputStream来帮助Scanner进行构造 // Scanner不仅可以从操作系统中读,也可以从文件,网络中读,Scanner放在while循环外也可以 Scanner scanner = new Scanner(inputStream); if(!scanner.hasNext()){ System.out.printf("[%s , %d],客户端断开连接\n",clientSocket.getInetAddress(),clientSocket.getPort()); break; } //8:根据请求计算响应 String request = scanner.next(); String response = process(request); //9:把响应返回给客户端(封装一下),进行写入 /* 直接用write方法写回响应,不方便添加换行符\n,因为客户端在读取响应的时候使用next,结束标志为\n、空格、tab outputStream.write(response.getBytes(),0,response.getBytes().length); */ PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); printWriter.flush(); System.out.printf("[%s,%d] , request: %s , response : %s\n " ,clientSocket.getInetAddress(),clientSocket.getPort(),request,response); } } catch (IOException e) { throw new RuntimeException(e); }finally { clientSocket.close(); } } public String process(String response){ return response; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } }
客户端
package InternetTcp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; /** * Created with IntelliJ IDEA. * Description: * User: Hua YY * Date: 2024-10-11 * Time: 17:01 */ public class TcpEchoClient { //1:创建一个Scoket对象 private Socket socket = null; //2:构造方法因为 TCP是有连接的 所以Scoket对象中包含服务器的IP和端口,实例化对象就会与服务器创立连接,服务器中的accept就会进行呼应 public TcpEchoClient(String serverIp , int serverPort) throws IOException { socket = new Socket(serverIp , serverPort); } public void start(){ System.out.println("客户端启动"); //一:获取控制台输入 //3:基于socket文件创建输入输出流,并实例化两个扫描器 try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){ Scanner scannerConsole = new Scanner(System.in);//控制台扫描器 Scanner scannerNetWork = new Scanner(inputStream); //5:调用PrintWriter中的write方法发送请求,用outputStream帮助构造 PrintWriter writer = new PrintWriter(outputStream); while(true){ System.out.print("->"); if (!scannerConsole.hasNext()){ break; } //二:构造请求,并发送 String request = scannerConsole.next(); writer.println(request);//呼应server中的获取响应scanner.next()//问题 writer.flush(); //三:接收响应 String response = scannerNetWork.next(); //四:在显示器上进行打印 System.out.println(response); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } }