当前位置: 首页 > news >正文

TCP网络编程

一)TCP Socket介绍:

1)TCP和UDP有着很大的不同,TCP想要进行网络通信的话首先需要通信双方建立连接以后然后才可以进行通信,TCP进行网络编程的方式和文件中的读写字节流类似,是以字节为单位的流进行传输

2)针对于TCP的套接字来说,JAVA本身提供了两个类来进行数据的传输,一个是ServerSocket,一个是Socket

2.1)ServerSocket:主要是给TCP服务器来进行使用的;

2.2)Socket:我们既需要给客户端来进行使用,也需要给服务器来进行使用;

这样就是说TCP是不需要使用专门的类来进行表示传输的包,因为TCP主要面向的是字节流,我们是以字节为单位进行传输的

一)ServerSocket类的介绍:

二)Socket类的介绍:

第二个类是Socket,这个类在客户端和服务器都会使用,用来进行客户端和服务器之间的数据传输通信,TCP的传输可以类似于打电话的场景,客户端发送请求以后服务器调用ServerSocket类的accept()方法来建立连接这样在建立连接之后客户端和服务器之间就可以进行通信了,Socket可以获取到文件也就是网卡的输入和输出流对象,然后就可以通过流对象来针对于网卡进行读写了,这就体现了TCP面向字节流,全双工的特点;

 ServerSocket的实例是ListenSocket,Socket的实例是clientSocket

如果有连接accept方法就会返回一个Socket对象,也就是说进一步的客户端和服务器的交互就交给Socket了;

1)也就是说咱们的ServerSocket就是一个接线员,他并不会进行负责具体的办理业务,而是把这个业务交给其他人来进行处理;

2)每当ServerSocket通过accept方法感知到了有一个客户端来和我们的服务器建立连接了,那么就会创建出一个Socket对象来进行通信,服务器针对每一个客户端,都会创建出一个Socket来进行通信;

1)由于咱们的TCP是有连接的,我们一进入到循环是不可以能开始读取数据,而是需要先进行和客户端建立连接,再进行传输数据,先进行接电话,而接电话的前提是有人给你打电话;只有说在客户端Socket中通过构造方法指定服务器的IP地址和端口号,这就相当于有人给你打电话,而咱们的服务器里面的ServerSocket中的accept方法就会感知到,并进行三次握手进行连接,就是相当于是接电话;
2)咱们的accept操作,如果说此时没有客户端和你建立连接,这个accept方法就会阻塞,直到有人向我们当前的服务器建立了连接;
3)因为客户端的请求的主机可能有多份,所以服务器针对每一个客户端主机都会有一个Socket对象来进行处理;

4)UDP的服务器进入主循环之后,就尝试用receive读取请求了,这是无连接的
5)但是我们的TCP是有连接的,首先需要做的事,先建立起连接,相当于客户端的代码是货品,就要通过一条公路来把这些货发送过去
6)当服务器运行的时候,是否有客户端来建立连接,不确定,如果客户端没有建立连接,accept就会进行阻塞等待,也就是说咱们的TCP必须先通过accept方法先建立好连接才可以进行传输数据,而我们的UDP完全不管37,21就直接进行发送数据

1)在一开始服务器进行启动的时候,我们就需要指定一个端口号,后续客户端要根据这个端口来进行访问,服务器的IP地址默认是主机IP
2)后续客户端进行访问服务器的时候,目的IP就是服务器的IP,不需要我们服务器的开发者来进行绑定了,只要我们确定服务器在一台电脑,IP地址就是确定了;

三)服务器如何获取到客户端的IP地址和端口号

当服务器的ServerSocket通过accept()方法和客户端建立了链接,那么此时客户端的信息就被保存到了Socket里面

1)可以直接通过ServerSocket实例调用accept方法返回的Socket对象的:

getInetAddress()方法获取到IP地址;

2)可以直接通过ServerSocket实例调用accept方法返回的Socket对象的getPort()方法就可以来进行获取到端口号;

四)客户端和服务器的通信原理:

咱们这里面针对的TCPSocket的读写就和文件读写是一模一样的;

1)在进行读Socket文件的数据就是相当于是在读取网络上面别的主机给咱们发送过来的数据

2)咱们在向Socket文件中写数据的时候,就是相当于是在网络上面向目标主机发送数据

通过调用Socket对象里面的getInputStream()方法,就可以进行获取到对应的流对象

全双工双向通信,既可以读,也是可以写的; 

五)服务器的读写过程:

1)再使用Socket对象clientSocket对象的getInputStream和getoutputStream对象得到一个字节流对象,这时就可以进行读取和写入了;此时的读操作可以调用inputstream.read()里面传入一个字节数组,然后再转化成String,但是比较麻烦,判定结束也是比较麻烦的,直到读到的结果是-1我们才会结束循环,所以优先使用Scanner来进行读取,这还是要多些循环,使用inputStream.read()的时候,就相当于是读取客户端发送过来的数据,也就是在读取网卡

2)通过Socket对象的clientSocket的getInputStream()来进行获取到流对象,但是具体读的时候Scanner 来进行具体的读,new Scanner(InputStreaam),就巧妙地读到了客户端的请求,在调用scan.next(),写的时候,直接利用Printstream new Printstream()构造方法里面直接写Outputstream,当调用println方法的时候,就默认写回到客户端了

3)当我们使用outputStream.write()方法的时候,就相当于向的客户端返回了数据

六)客户端读写文件的过程:

1)创建一个socket对象,创建的时候同时指定服务器的IP和端口号,然后把传入到socket中,这个过程就会让客户端和服务器建立连接,这就是三次握手,这个过程就是内核完成的;

2)这时客户端就可以通过Socket对象中的getInputStream和getOutputstream,来得到一个字节流对象,这时就可以与服务器进行通信了;

3)在读的时候,要注意此时只写一个Socket文件,直接把服务器的端口号和IP地址,传入进去进行构造,就自动地和客户端进行了连接;此时还要有InputStreamSocket和OutstreamSocket;

七)关于关闭Socket文件

1)对于accept的返回值对应Socket这个文件,是有一个close方法的,如果打开一个文件后,是要记得去关闭文件的,如果不关闭,就可能会造成文件资源泄漏的情况,一个socket文件写完之后本应是要关闭的,但是咱们前面写的UDP程序是不可以关闭的,当时的socket是有生命周期的,都是要跟随整个程序的,当客户端不在工作时,进程都没了,PCB也没了,文件描述符就更没有了

2)咱们的TCP服务器有一个listenSocket,但是会有多个clientsocket,可以说每一个客户端,每一个请求都对应一个clientSocket,如果这个客户端断开链接了,对应的clientSocket也就需要销毁了

1)TCP服务器的时候,都针对了这里面的climentSocket(Socket创建的实例)关闭了一下,但是我们对于listenSocket(ServerSocket创建的实例)却没有进行关闭,直到服务器进行关闭;

2)同时在UDP的代码里面也没有针对DatagramSocket对象和DatagramPacket来进行关闭

catch (IOException e) {e.printStackTrace();}finally {clientSocket.close();//listenSocket.close();在这里面是不能进行关闭的}

1)关闭的目的是为了释放资源,释放资源的一个前提是这个资源已经不再进行使用了,对于咱们的UDP的程序和ServerSocket来说,这些Socket都是贯穿程序始终的,只要程序启动运行我们就要用到,什么时候咱们的服务器进程关闭,什么时候不用;但是咱们的服务器针对每一个客户端的Socket文件,什么时候客户端断开链接了,啥时候就不会再进行使用了

2)这些实例什么时候不用?啥时候咱们的服务器进行关闭,啥时候不用;

3)咱们的这些资源最迟最迟也就会随着进程的退出一起进行释放了,进程才是操作系统分配资源的最小单位,那么这个进程曾经进行申请的资源也就没有了;

4)但是咱们的clientSocket的生命周期是很短的,针对咱们的每一个客户端程序,都要进行创建一个climentSocket,当对应的客户端断开连接之后,咱们的服务器的对应的客户端的climentSocket对象也就永远不会再进行使用了,就需要关闭文件释放资源,咱们的climentSocket对象有很多,每一个客户端都对应一个Socket对象,就需要保证,每一次进行处理完成的连接就必须进行释放

 八)TCP服务器设计:

1)创建ServerSocket实例对象,需要指定服务器的端口号

2)启动服务器,使用accept方法和客户端建立连接,如果没有客户端建立连接,那么这里面的accept方法会阻塞

3)接受客户端的请求,通过Socket对象的获取到InputStream来读取请求

4)除了客户端请求,计算响应

5)将响应返回给客户端,通过Socket获取到OutPutStream流对象来发送响应

package com.example.demo.Controller.Socket;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 TcpServer {public ServerSocket listenSocket=null;public int serverPort;public TcpServer(int serverPort) throws IOException {this.serverPort=serverPort;this.listenSocket=new ServerSocket(serverPort);}public void start() throws IOException {System.out.println("TCP服务器已经启动");while(true){//对于每一个客户端来说,服务器都会返回一个clientSocketSocket clientSocket=listenSocket.accept();//用来处理对应的客户端的请求procession(clientSocket);}}private void procession(Socket clientSocket) throws IOException {//此时使用了try catch这种写法,就不需要手动关闭inputStream和OutPutStream的资源了System.out.println("当前客户端和服务器建立了链接,IP地址和端口号是"+clientSocket.getInetAddress().getHostAddress()+clientSocket.getPort());try(InputStream  inputStream=clientSocket.getInputStream()){try( OutputStream outputStream=clientSocket.getOutputStream()) {Scanner scanner = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);while (true) {if(!scanner.hasNext()){//当这个服务器收不到客户端的请求的时候,此时就会跳出循环,和客户端断开连接System.out.println("当前客户端断开连接"+clientSocket.getPort()+clientSocket.getInetAddress());break;}//1.读取客户端的请求并解析String request = scanner.next();//2.处理请求String response = processMessage(request);//3.返回响应writer.println(response);writer.flush();}}}catch (IOException e){e.printStackTrace();}finally {clientSocket.close();}}private String processMessage(String request) {return request;}public static void main(String[] args) throws IOException {TcpServer server=new TcpServer(9090);server.start();}
}

1)这里面的hashNext方法会进行判断输入,文件,字符串,键盘等输入流,是否还存在着下一个输入,如果有,那么返回true,如果没有,那么直接返回false,hasNext本身会等待客户端那边的输入,也会阻塞等待输入源的输入,当客户端那一边关闭了链接,输入源也就结束,没有下一个数据,说明读完了,此时的hasNext就返回了false

2)next方法是一直读取到换行符/空格/其他空白符结束,但是最终返回结果里不包含空白符

3)这里面的hasNext()什么时候会返回false呢?这是因为当客户端退出以后,对应的流对象就读取到了EOF,也就是文件结束标记,那么这里面为啥会读到EOF呢?这是因为客户端进程在进行退出的时候,就会触发socket.close()也就是会触发FIN,这个客户端关闭链接的请求,也就是操作系统内核收到来自于客户端发送过来的FIN数据包,那么就会将输入源结束,标记成EOF;

4)上面实现的TCP回显服务器的代码中有一个致命的缺陷就是, 这个代码同一时间只能连接一个客户端, 也就是只能处理一个客户端的请求

public class TCPServer {ServerSocket listenSocket=null;public TCPServer(int serverPort) throws IOException {listenSocket=new ServerSocket(serverPort);}public void start() throws IOException {System.out.println("TCP服务器开始进行启动");while(true){Socket clientSocket= listenSocket.accept();procession(clientSocket);}}private void procession(Socket clientSocket) throws IOException {System.out.printf("我们这一次客户端请求的IP地址是%s 端口号是%d",clientSocket.getInetAddress().toString(),clientSocket.getPort());try(InputStream inputStream=clientSocket.getInputStream()){try(OutputStream outputStream= clientSocket.getOutputStream()){
//我们来进行循环处理请求,来进行处理响应,我们的一台主机是要给服务器发送多次请求的Scanner scanner=new Scanner(inputStream);while(true){if(!scanner.hasNext()){
//客户端断开连接的代码System.out.printf("客户端断开连接%s %d",clientSocket.getInetAddress(),clientSocket.getPort());break;}}
我们在这里面使用Scanner是更方便的,如果说我们不使用Scanner就需要进行使用原生的inputStream中的read方法就可以了,只不过我们需要创建一个字节数组,然后使用stringbulider来进行拼接
// 1.读取请求并进行解析,读取Socket网卡String request= scanner.next();
//2.根据请求计算并执行逻辑,我们创建process方法执行String response=process(request);
//3.帮我们写的逻辑返回给客户端,为了方便起见,我们直接使用PrintWriter来进行对OutputStream来进行包裹一下PrintWriter printWriter=new PrintWriter(outputStream);printWriter.println(response);printWriter.flush();
//4打印信息
System.out.printf("[客户端的端口号是%d 客户端的IP地址是%s],请求数据是%s,响应数据是%s",clientSocket.getPort(),clientSocket.getInetAddress(),request,response);}} catch (IOException e) {e.printStackTrace();}finally {clientSocket.close();listenSocket.close();}}private String process(String request) {return request+"我爱你";}public static void main(String[] args) throws IOException {TCPServer server=new TCPServer(9099);server.start();}
}
九)TCP客户端设计:

1)创建Socket实例对象,用于和服务器建立连接,参数是服务器的IP地址和端口号,在进行new Socket的过程中,就会触发TCP三次握手

2)客户端启动, 用户输入请求构造构造请求并发送给服务器(使用OutputStream/PrintWriter), 要注意去刷新缓冲区保证数据成功写入网卡

3)读取服务器的响应并进行处理.

package com.example.demo.Controller.Socket;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 TcpClient {public Socket clientSocket;public String serverIP;public int serverPort;public TcpClient(String serverIP,int serverPort) throws IOException {this.serverPort=serverPort;this.serverIP=serverIP;this.clientSocket=new Socket(serverIP,serverPort);}public void start() throws IOException {System.out.println("客户端开始启动"+clientSocket.getPort()+clientSocket.getInetAddress().getHostAddress());try(InputStream inputStream=clientSocket.getInputStream()) {try(OutputStream outputStream=clientSocket.getOutputStream()){Scanner scanner=new Scanner(System.in);Scanner clientScanner=new Scanner(inputStream);PrintWriter writer=new PrintWriter(outputStream);while(true){//1.客户端输入命令System.out.println("客户端请输入命令--->");String request=scanner.next();if(request.equals("exit")){System.out.println("客户端退出");return;}//2.客户端发送命令writer.println(request);writer.flush();//3.客户端接收响应String response=clientScanner.next();System.out.println(request);System.out.println("服务器返回的数据是"+response);}}} catch (IOException ex) {throw new RuntimeException(ex);}finally {clientSocket.close();}}public static void main(String[] args) throws IOException {TcpClient client=new TcpClient("127.0.0.1",9090);client.start();}
}

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;class Request{int serverport=0;String serverIp=null;Socket socket=null;public  Request(String serverIp,int serverport)throws IOException {this.serverIp=serverIp;this.serverport=serverport;this.socket=new Socket(serverIp,serverport);}public void start()throws IOException {
//1从键盘也就是控制台上读取请求Scanner scan = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {while (true) {System.out.println("请输入你的请求内容");System.out.println("->");String request = scan.next();if (request.equals("goodbye")) {System.out.println("即将退出客户端");break;}//2把这个从键盘读取的内容,构造请求发给服务器PrintStream printStream=new PrintStream(outputStream);printStream.println(request);
//在这里我们怀疑println不是把数据发送到服务器中了,而是放到缓冲区里面了,我们刷新一下缓冲区,强制进行发送printStream.flush();//如果不刷新,服务器无法及时收到数据//3我们从服务器那边读取响应,并进行解析Scanner scanreturn=new Scanner(inputStream);String response=scanreturn.next();
//4我们把结果显示在控制台上面String string=String.format("请求是 %s,回应是 %s",request,response);System.out.println(string);}}catch(IOException e){e.printStackTrace();}}public static void main(String[] args) throws IOException {//此处构建request对象的时候就相当于是和服务器建立连接Request request=new Request("127.0.0.1",9090);request.start();}
}

1)让Socket创建的同时,就与服务器建立了链接,相当于拨电话的操作,这个操作对标于服务器中的climentSocket.accept接电话操作,客户端的IP地址就是本机的IP地址,端口号是由系统自动进行分配的;
2)在这里面传入的IP和端口号的含义表示的是,不是自己进行绑定,而是表示和这个IP端口建立连接 

咱们的服务器要想拿到客户端的端口号就要通过climentSocket(Socket创建的实例)

IP地址:climentSocket.getInetAddress().toString();

端口号:climentSocket.getPort();

要使用printWriter中的println方法而不是write();

1)针对写操作,要进行刷新缓冲区,如果没有这个刷新,客户端时不能第一时间获取到这个响应结果

2)对于每一次请求,都对应着一个Socket,都要创建一个procession方法来进行处理,接下来就来处理请求和响应
3)这里面的针对TCP Socket的文件读写是和文件读写是一模一样的,socket文件来进行读和写,TCP和UDP是全双工,既可以读Socket文件,也可以写Socket文件

十)如何进行判断是否断开连接?建立连接

1.1)咱们的服务器会在进行处理每一个客户端的请求的时候,会进行判定if(scanner.hasNext)判断我们的客户端是否已经读取完成了,如果我们的客户端断开连接,那么我们的if判定就会返回,服务器读取就会完毕

1.2)如果客户端已经连接了,那么ServerSocket的accept()方法就会返回,如果客户端断开连接了,那么我们的服务器的hasNext()方法就会感知到; 

十一)基于TCP的网络计算机:

进行运算,要吃CPU,进行传输,吃的是网络宽带;

客户端给服务器发送的请求:
字符串:第一个操作数,第二个操作数,运算符;
服务器给客户端返回的响应:计算结果
下面是实现运算服务器的代码
所谓的自定义协议,一定是在开发之前,就约定好的,开发过程中,就要让客户端和服务器,都能够严格遵守协议约定好的格式,直接使用文本+分隔符,假设传输的请求和响应中,各自有几十个字段,况且有些字段是可选的;

package demo2;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;public class TcpRequestRequest {public String serverIP;public int serverPort;private Socket socket=null;public TcpRequestRequest(String serverIP, int serverPort) throws IOException {this.serverIP=serverIP;this.serverPort=serverPort;this.socket=new Socket(serverIP,serverPort);}public void start(){System.out.println("客户端开始进行启动");try(InputStream inputStream=socket.getInputStream()) {try (OutputStream outputStream = socket.getOutputStream()) {while(true){
//1.先进行输入数据Scanner scanner=new Scanner(System.in);System.out.println("请输入第一个操作数");String s1=scanner.nextLine();System.out.println("请输入操作符");String s2=scanner.nextLine();System.out.println("请输入第二个操作数");String s3=scanner.nextLine();String request=s1+"B"+s2+"B"+s3;
//2.发送数据//2.1再进行发送数据PrintStream printStream = new PrintStream(outputStream);printStream.println(request);printStream.flush();//2.2再进行接受服务器的响应Scanner result = new Scanner(inputStream);String response = result.nextLine();//2.3打印在控制台上面System.out.println(response);System.out.println("是否接下来要继续进行计算?");String next = scanner.nextLine();if (next.equals("N")) {System.out.println("即将退出客户端");break;}}
}
} catch (IOException e) {e.printStackTrace();
}}public static void main(String[] args) throws IOException {TcpRequestRequest request=new TcpRequestRequest("127.0.0.1",9999);request.start();}
}
package demo2;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 TcpClimentServer {public int serverPort;private ServerSocket listensocket=null;public TcpClimentServer(int serverPort) throws IOException {this.serverPort=serverPort;this.listensocket=new ServerSocket(serverPort);}public void start() throws IOException {System.out.println("开始启动我们的服务器");while(true){Socket socket=listensocket.accept();System.out.printf("客户端的IP地址是%s,客户端的端口号是%d",socket.getInetAddress(),socket.getPort());procession(socket);}}private void procession(Socket socket) {try(InputStream inputStream=socket.getInputStream()) {try(OutputStream outputStream=socket.getOutputStream()){while(true){
//1.我们先进行读取客户端的请求Scanner scanner=new Scanner(inputStream);String request=scanner.nextLine();
//2.根据请求进行处理业务逻辑String response=Process(request);
//3.根据业务逻辑来进行计算响应PrintWriter writer=new PrintWriter(outputStream);writer.println(response);writer.flush();
//4.显示计算结果System.out.printf("客户端的请求是%s 服务器返回的响应是%s",request,response);}} catch (IOException e) {e.printStackTrace();}} catch (IOException e) {e.printStackTrace();}}public String Process(String request){//1.判断数据是否为空if(request==null||request.equals("")){return "当前传输数据有误,什么也没有传输过来";}//2.按照标点符号来进行分割String[] strings=request.split("B");if(strings.length!=3){return "您当前数据所进行输入的格式是存在错误的";}//3.取出里面的数字和符号来进行运算int result=0;Integer s1=Integer.parseInt(strings[0]);Integer s2=Integer.parseInt(strings[2]);String operate=strings[1];if(operate.equals("+")){result=s1+s2;}else if(operate.equals("-")){result=s1-s2;}else if(operate.equals("*")){result=s1*s2;}else{result=s1/s2;}return String.valueOf(result);}public static void main(String[] args) throws IOException {TcpClimentServer server=new TcpClimentServer(9999);server.start();}
}
十二)当前TCP服务器同一时刻只能进行处理一个客户端的请求: 

现象:一个服务器对应多个客户端,此时就需要多次启动这个客户端实例;

现象是当我们启动第一个客户端之后,服务器进行提示上线,当我们启动第二个客户端的时候,服务器此时就没有任何响应了,况且发送请求的时候没有进行任何响应;但是当我们退出客户端的时候,此时神奇的事情出现了,服务器提示了客户端二上线,况且客户端二也得到了服务器的响应,但是此时客户端三没有任何反应,当我们把客户端2的主机退出,那么客户端3给服务器发送的数据就有效了,所以当前的服务器,在同一时刻,只可以给一个客户端提供服务,只有前一个客户端下了,下一个客户端才可以上来;

原因:第一个客户端尝试与服务器建立连接的时候,服务器会与客户端建立连接,这个时候客户端发送数据,服务器就会做出响应,客户端多次发送数据,服务器就会循环处理请求

3.1)调用listenSocket的accept方法,与客户端建立连接;

3.2)执行process方法,来循环进行处理客户端给服务器发送过来的请求,除非说第一个客户端断开链接了,否则就无法进行处理其他请求了,因为它陷在我们的一个procession方法里面的while(sc.hashNext())方法无法出来了;

3.3)第二个,第三个,第四个客户端想要给服务器发送数据,不可能成功建立连接;

1)原因是在服务器中的hasNext那里在等待第一个客户端发送数据,服务器本身并没有退出第一个客户端对应的这个procession这个方法,也就是说直接死在第一个客户端的procession这个方法的while循环里面(一直进行工作),所以整个服务器的程序就卡死在hasNext这个代码块里面了,这样就导致主函数的外层的while循环无法进行下一轮,也就无法重新进行循环调用第二次accept方法,服务器无法再次调用accept方法与下一个客户端进行三次握手,建立连接;

2)最终造成的结果是,客户端什么时候退出,hasNext()方法就进行返回,procession()方法就进行返回,第一次外层循环结束,进行第二层外层循环,才有可能继续调用accept()方法

3)所以问题的关键在于,如果第一个客户端没有退出,此时服务器的逻辑就一直在procession里面打转,也就没有机会进行外层循环再次调用accept方法,也就无法再次去处理第二个连接,第一个客户端退出以后,结束里面的循环,结束上一个procession,服务器才可以执行到第二个accept,才可以建立连接;

3)这个问题就类似于,好像你接了一个电话,和对方你一言,我一语的进行通话,别人再继续给我们进行打电话,我们就没有办法进行接通了

4)咱们解决上述问题,就需要第一次执行的procession方法,不能影响到咱们的下一次循环扫描accept的执行;

 

1)咱们的accept方法调用一次,就接通一个,如果说多的调用几次,我们就可以多接通几个,所以解决方法就是说:咱们的调用procession方法和accept方法执行的调用不会相互干扰

2)也就是说不能让咱们的procession方法里面的循环影响到前面accept方法的执行;

3)怎么样才可以说让我们的procession方法自己去执行自己的,并且让这个accept执行自己的呢?让我们的accept被反复调用到,又让我们的procession来进行反复地进行处理客户端请求呢?

1)引入多线程之后,保证主线程始终在调用accept,每次都有一个新的连接来创建新线程来处理请求响应,线程都是一个独立的执行流每一个线程都会执行自己的同一段逻辑并发执行 

2)咱们调用accept方法的线程和调用procession方法的线程是互不干扰的呀

让主线程主线程专门负责进行accept和客户端建立连接, 每收到一个连接, 创建新的线程, 由新线程来负责处理这个新的客户端请求

3)使用线程池相较于多线程是更优化的方案, 使用多线程的话每连接到一个客户端就会就会创建一个线程, 如果同一时刻连接过多, 这里线程创建和销毁的开销就比较大了, 使用线程池就可以减少这里开销, 提高效率


public class Server {HashMap<String,String> map=new HashMap<>();int serverport=0;ServerSocket listenSocket=null;public Server(int serverport) throws IOException {this.serverport=serverport;this.listenSocket=new ServerSocket(serverport);map.put("及时雨","宋江");map.put("国民女神","高圆圆");map.put("老子","李佳伟");}public void start() throws IOException {System.out.println("服务器即将启动");while(true){Socket climentSocket=listenSocket.accept();
//我们的改进方案是每一次accept方法成功,那么我们就创建一个新的线程,有新的线程负责来执行这次process方法,这样就实现了代码之间的解耦合Thread thread=new Thread(){public void run(){String str=String.format("客户端的IP地址是 %s 客户端对应的端口号是 %d",climentSocket.getInetAddress().toString(),climentSocket.getPort());System.out.println(str);try {procession(climentSocket);} catch (IOException e) {e.printStackTrace();}}};thread.start();}}public void procession(Socket climentSocket) throws IOException {try(InputStream inputStream=climentSocket.getInputStream();OutputStream outputStream=climentSocket.getOutputStream()){Scanner scanner=new Scanner(inputStream);PrintStream printStream=new PrintStream(outputStream);while(true){if(!scanner.hasNext()){System.out.println("这个客户端对应的服务器已经完成了工作");return;}String request=scanner.next();String response=hhhh(request);printStream.println(response);String str=String.format("请求是 %s 响应是 %s",request,response);System.out.println(str);}}}public String hhhh(String request){return map.getOrDefault(request,"没有这个参数");}
这是改进后的服务器代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Server {HashMap<String,String> map=new HashMap<>();int serverport=0;ServerSocket listenSocket=null;public Server(int serverport) throws IOException {this.serverport=serverport;this.listenSocket=new ServerSocket(serverport);map.put("及时雨","宋江");map.put("国民女神","高圆圆");map.put("老子","李佳伟");}public void start() throws IOException {System.out.println("服务器即将启动");while(true){Socket climentSocket=listenSocket.accept();String str=String.format("客户端的IP地址是 %s 客户端对应的端口号是 %d",climentSocket.getInetAddress().toString(),climentSocket.getPort());System.out.println(str);ExecutorService executorService= Executors.newCachedThreadPool();executorService.submit(new Runnable() {@Overridepublic void run() {try {procession(climentSocket);} catch (IOException e) {e.printStackTrace();}}});}}public void procession(Socket climentSocket) throws IOException {try(InputStream inputStream=climentSocket.getInputStream();OutputStream outputStream=climentSocket.getOutputStream()){Scanner scanner=new Scanner(inputStream);PrintStream printStream=new PrintStream(outputStream);while(true){  if(!scanner.hasNext()){System.out.printf("客户端程序 %s,客户端的端口号是%d,断开连接",climentSocket.getInetAddress(),climentSocket.getPort());return;}String request=scanner.next();String response=hhhh(request);printStream.println(response);String str=String.format("请求是 %s 响应是 %s",request,response);System.out.println(str);}}}public String hhhh(String request){return map.getOrDefault(request,"没有这个参数");}public static void main(String[] args) throws IOException {Server server=new Server(8080);server.start();}
}

 

1)这样子咱们的主线程第一次循环之后调用accept方法,就会立刻创建线程,立即调用start方法,接下来就会又会执行start方法中的循环;

2)主要就是说线程就是一个独立的执行流,线程之间是并发执行的,在我们的start方法在进行执行accept方法进行返回的时候,立即就会创建新线程,同时本次循环结束;在新线程里面我们就会不断地接受此次客户端的请求;

1)咱们刚才的UDP版本的程序就没有用到多线程?因为咱们的UDP编程不需要处理连接,咱们的UDP只需要一个循环,就可以处理所有客户端的请求;

2)DatagramPacket requestpacket=new DatagramPacket(new Byte[4096],4096);

requestSocket.receive(requestPacket),这个start方法进入循环,不管谁,不管哪一个客户端发送过来了请求,服务器都会进行处理返回,一个循环把所有客户端都给伺候好了

3)但是咱们的TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环,里层循环就会影响外层循环的进度了,况且两个循环是在两个不同的方法里面;

4)主线程循环调用accept方法,当我们有客户端尝试进行连接的时候,直接让主线程创建一个新线程,由多个新线程负责并发处理若干个客户端的请求,在新线程里面,我们通过while循环来进行处理请求,这个时候,多线程就是并发执行的关系了,就是各自执行各自的,彼此之间不会相互干扰

5)在主循环里面,会循环的调用accept方法,在每一个新线程里面会进行循环的读取客户端的请求,这样就做到了把两个循环拆分成了两个不同的线程,宏观上面,两个线程各自去执行各自的,就是一个并发执行的关系,因此也就不会相互影响的;

6)但是在实际开发中,客户端的数目可能有很多,每一个客户端进行连接都要分配一个线程

虽然线程比进程更轻量,但是如果有很多的客户端连接又退出,这就会导致咱们当前服务器频繁的创建销毁线程,系统的开销和系统的负荷量是很大的,那么我们如何改进这个问题,这时我们就需要用到线程池了;

7)当客户端new Socket()成功的时候,其实本质上从操作系统内核层面,已经建立好了连接(TCP三次握手),但是咱们的应用程序没有接通这个链接,应用程序没有进行这个接听动作,也没有办法真的去说话;

8)改成多线程版本之后,虽然咱们procession的代码已经进入到处理连接的阶段了,但是并不会影响去调用accept方法,当前的这个问题就是会说其实是电话打过去了,但是对方没有进行接听,尝试进行建立连接的请求,已经发送过去了,对方也知道了,也就是说咱们的操作系统内核已经把工作做好了,但是我们的上层应用程序还是不想去接触;

假设极端情况下,一个服务器面临着很多很多客户端,这些客户端,连接上了并没有退出,这个时候服务器这边,就会存在很多很多线程,会有上万个线程?这个情况下,会有一些其他的问题吗?这是科学的解决方法吗?

1)实际上这种现象是不安全的,不科学,每一个线程都会占据一定的系统资源,如果线程太多太多了,这时候许多系统资源(内存资源+CPU资源)就会十分紧张,达到一定程度,机器可能就会宕机,因为你创建线程就要有PCB,还要为这个线程分配栈和程序计数器的空间;

2)上万个线程会抢这一两个CPU,操作系统的内核就会十分激烈,线程之间的调度开销是十分激烈的,比如说线程1正在执行,执行一会被操作系统的内核调度出去了,下一次啥时候上CPU执行,就不知道了,因为排的队,实在是太多太多了,线程之间非常卷,服务器对于客户端的响应能力,返回数据的时间,就会大大降低了;

3)例如在双十一/春运这种场景,如果一个系统,同时收到太高的并发,就可能会出现问题,例如每一个并发都需要消耗系统资源,并发多了,系统资源消耗就多了,系统剩下的资源就少了,响应能力就变慢了,再进一步,把系统都给消耗没了,系统也就无法正常工作了(我们的服务器不可能响应无数个客户端)

1)使用携程来进行代替线程,完成并发,很多协程的实现,是一个M:N的关系(一大堆的携程是通过一个线程来进行完成的),协程比线程还要轻量

2)使用IO多路复用的机制,完成并发;

2.1)这会从根本上来解决服务器高并发的这样一个问题,在内核里面来支持这样的功能

2.2)假设现在有1W个客户端,在这个服务器里面就会用一定的数据结构把1W个客户端对应的Socket保存好,不需要一个线程对应一个客户端,一共就有几个线程,IO多路复用机制,就可以做到,哪个Socket上面有数据了,就通知到这个应用程序,让这个线程从这个socket里面来读数据,虽然是1w个客户端,但是在同一时刻,也就只有不到1K个客户端来给服务器发送请求,靠系统来通知应用程序,谁可以读,就去读谁,通过一个线程就可以处理多个Socket,在C++方向,典型实现就是epoll(mac/linux),kqueue(windows),我们在Java里面通过NIO这样的一系列的库,封装了epoll等IO多路复用机制;

3)使用多个服务器(分布式),就算我们使用协程,就算使用IO多路复用,咱们的系统同一时刻处理的线程数仍然是有限的,只是节省了一个客户端对应的资源,但是随着客户端的增加,还是会消耗更多的资源,我们就使用更多的硬件资源,此时每一台主机承受的压力就小了

以上三种都是一个基本处理高并发场景的,所使用的方法;

 

相关文章:

TCP网络编程

一)TCP Socket介绍: 1)TCP和UDP有着很大的不同&#xff0c;TCP想要进行网络通信的话首先需要通信双方建立连接以后然后才可以进行通信&#xff0c;TCP进行网络编程的方式和文件中的读写字节流类似&#xff0c;是以字节为单位的流进行传输 2)针对于TCP的套接字来说&#xff0c;J…...

K8S知识点(九)

&#xff08;1&#xff09;Pod详解-结构和定义 一级属性有下面这些&#xff1a;前两个属性是字符串&#xff0c;上面有定义 kind&#xff1a;Pod version&#xff1a;v1 下面的属性是object 还可以继续查看子属性&#xff1a;二级属性 还可以继续查看三级属性&#xff1a; 通…...

el-table实现单选和隐藏全选框和回显数据

0 效果 1 单选 <el-table ref"clientTableRef" selection-change"clientChangeHandle"><el-table-column fixed type"selection" width"50" align"center" /><el-table-column label"客户名称" a…...

香港科技大学广州|智能制造学域机器人与自主系统学域博士招生宣讲会—中国科学技术大学专场

&#x1f3e0;地点&#xff1a;中国科学技术大学西区学生活动中心&#xff08;一楼&#xff09;报告厅 【宣讲会专场1】让制造更高效、更智能、更可持续—智能制造学域 &#x1f559;时间&#xff1a;2023年11月16日&#xff08;星期四&#xff09;18:00 报名链接&#xff1a…...

[P7885][Android13] 解决5G信号良好状态栏信号只有两格的问题

文章目录 开发平台基本信息问题描述解决方法 开发平台基本信息 芯片: 展锐P7885 版本: Android 13 kernel: kernel-5.15 问题描述 最近有一款预研设备使用的是展锐 P7885 的5G 智能模组&#xff1b;经过天线厂调试天线后&#xff0c;各项指标都达到了标准&#xff0c;正常待…...

老版本goland无法调试新版本go问题处理

背景 无法调试1.20版本b 报错如下&#xff1a; No goroutine selected 懒人不想升级goland版本。 处理方法 1.安装最新的dlv工具 go install github.com/go-delve/delve/cmd/dlvlatest 2.找到刚刚安装的dlv工具&#xff0c;并复制 # 位于$GOPATH的bin目录下&#xff0c;如…...

Redis应用之二分布式锁2

一、前言 前一篇 Redis应用之二分布式锁 我们介绍了使用SETNX来实现分布式锁&#xff0c;并且还遗留了一个Bug&#xff0c;今天我们对代码进行优化&#xff0c;然后介绍一下Redisson以及数据库的乐观锁悲观锁怎么用。 二、SetNX分布式锁优化后代码 RedisService.java Inven…...

打印字符(C++)

系列文章目录 进阶的卡莎C++_睡觉觉觉得的博客-CSDN博客数1的个数_睡觉觉觉得的博客-CSDN博客双精度浮点数的输入输出_睡觉觉觉得的博客-CSDN博客足球联赛积分_睡觉觉觉得的博客-CSDN博客大减价(一级)_睡觉觉觉得的博客-CSDN博客小写字母的判断_睡觉觉觉得的博客-CSDN博客纸币(…...

React函数组件的使用(Hooks)

目录 Hooks概念理解 1. 什么是hooks 2. Hooks解决了什么问题 useState 1. 基础使用 2. 状态的读取和修改 3. 组件的更新过程 4. 使用规则 useEffect 1. 理解函数副作用 2. 基础使用 3. 依赖项控制执行时机 4. 清理副作用 Hooks概念理解 本节任务: 能够理解hooks的…...

一篇博客读懂队列——Queue

目录 一、队列的概念和结构 ​二、队列的实现 2.1队列的初始化QueueInit 2.2队列的摧毁QueueDestroy 2.3插入结点QueuePush 2.4删除结点QueuePop 2.5返回队头QueueFront 2.6返回队尾QueueBack 2.7判断队列为空QueueEmpty 2.8统计队列数目QueueSize 一、队列的概念和…...

Effective C++ 系列和 C++ Core Guidelines 如何选择?

Effective C 系列和 C Core Guidelines 如何选择&#xff1f; 如果一定要二选一&#xff0c;我会选择C Core Guidelines。因为它是开源的&#xff0c;有300多个贡献者&#xff0c;而且还在不断更新&#xff0c;意味着它归纳总结了最新的C实践经验。最近很多小伙伴找我&#xff…...

Sandbox: bash(5613) deny(1) file-write-create 错误解决

Showing Recent Errors Only Sandbox: bash(5613) deny(1) file-write-create /Users/xx/Dev/UniappLearn/MSLUniappDemo/Pods/resources-to-copy-MSLUniappDemo.txt image.png 解决方法 build setting搜索ENABLE_USER_SCRIPT_SANDBOXING&#xff0c;YES&#xff08;默认&…...

腾讯云标准型S5服务器五年优惠价格表(4核8G和2核4G)

腾讯云服务器网整理五年云服务器优惠活动 txyfwq.com/go/txy 配置可选2核4G和4核8G&#xff0c;公网带宽可选1M、3M或5M&#xff0c;系统盘为50G高性能云硬盘&#xff0c;标准型S5实例CPU采用主频2.5GHz的Intel Xeon Cascade Lake或者Intel Xeon Cooper Lake处理器&#xff0c;…...

Nginx 是如何解决惊群效应的?

什么是惊群效应&#xff1f; 第一次听到的这个名词的时候觉得很是有趣&#xff0c;不知道是个什么意思&#xff0c;总觉得又是奇怪的中文翻译导致的。 复杂的说&#xff08;来源于网络&#xff09;TLDR; 惊群效应&#xff08;thundering herd&#xff09;是指多进程&#xff…...

【深度学习实验】网络优化与正则化(三):随机梯度下降的改进——Adam算法详解(Adam≈梯度方向优化Momentum+自适应学习率RMSprop)

文章目录 一、实验介绍二、实验环境1. 配置虚拟环境2. 库版本介绍 三、实验内容0. 导入必要的库1. 随机梯度下降SGD算法a. PyTorch中的SGD优化器b. 使用SGD优化器的前馈神经网络 2.随机梯度下降的改进方法a. 学习率调整b. 梯度估计修正 3. 梯度估计修正&#xff1a;动量法Momen…...

如何解决网页中的pdf文件无法下载?pdf打印显示空白怎么办?

问题描述 偶然间&#xff0c;遇到这样一个问题&#xff0c;一个网页上的附件pdf想要下载打印下来&#xff0c;奈何尝试多种办法都不能将其下载下载&#xff0c;点击打印出现的也是一片空白 百度搜索了一些解决方案都不太行&#xff0c;主要解决方案如&#xff1a;https://zh…...

【JVM】类加载器 Bootstrap、Extension、Application、User Define 以及 双亲委派

以下环境为 jdk1.8 两大类 分类成员语言继承关系引导类加载器bootstrap 引导类加载器C/C无自定义类加载器extension 拓展类加载器、application 系统/应用类加载器、user define 用户自定义类加载器Java继承于 java.lang.ClassLoader 四小类 Bootstrap 引导类加载器 负责加…...

读书笔记:彼得·德鲁克《认识管理》第15章 使工作富有成效:工作和过程

一、章节内容概述 不同员工在技术熟练程度、知识掌握程度方面有所不同&#xff0c;但所有工 作本质上都是相同的&#xff0c;为了实现富有成效&#xff0c;需要遵循同样的步骤&#xff0c;划分 为同样的阶段&#xff0c;受到同样的对待&#xff0c;需要分析、综合、控制以及相…...

媒体软文投放的流程与媒体平台的选择

海内外媒体软文&#xff1a;助力信息传播与品牌建设 在当今数字化时代&#xff0c;企业如何在庞大的信息海洋中脱颖而出&#xff0c;成为品牌建设的领军者&#xff1f;媒体软文投放无疑是一项强大的策略&#xff0c;通过选择合适的平台&#xff0c;精准投放&#xff0c;可以实…...

【excel技巧】如何取消excel隐藏?

Excel工作表中的行列隐藏了数据&#xff0c;如何取消隐藏行列呢&#xff1f;今天分享几个方法给大家 方法一&#xff1a; 选中隐藏的区域&#xff0c;点击右键&#xff0c;选择【取消隐藏】就可以了 方法二&#xff1a; 如果工作表中有多个地方有隐藏的话&#xff0c;还是建…...

大数据学习栈记——Neo4j的安装与使用

本文介绍图数据库Neofj的安装与使用&#xff0c;操作系统&#xff1a;Ubuntu24.04&#xff0c;Neofj版本&#xff1a;2025.04.0。 Apt安装 Neofj可以进行官网安装&#xff1a;Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练

前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1)&#xff1a;从基础到实战的深度解析-CSDN博客&#xff0c;但实际面试中&#xff0c;企业更关注候选人对复杂场景的应对能力&#xff08;如多设备并发扫描、低功耗与高发现率的平衡&#xff09;和前沿技术的…...

多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验

一、多模态商品数据接口的技术架构 &#xff08;一&#xff09;多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如&#xff0c;当用户上传一张“蓝色连衣裙”的图片时&#xff0c;接口可自动提取图像中的颜色&#xff08;RGB值&…...

《通信之道——从微积分到 5G》读书总结

第1章 绪 论 1.1 这是一本什么样的书 通信技术&#xff0c;说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号&#xff08;调制&#xff09; 把信息从信号中抽取出来&am…...

在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?

uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件&#xff0c;用于在原生应用中加载 HTML 页面&#xff1a; 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...

LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》

这段 Python 代码是一个完整的 知识库数据库操作模块&#xff0c;用于对本地知识库系统中的知识库进行增删改查&#xff08;CRUD&#xff09;操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 &#x1f4d8; 一、整体功能概述 该模块…...

STM32---外部32.768K晶振(LSE)无法起振问题

晶振是否起振主要就检查两个1、晶振与MCU是否兼容&#xff1b;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容&#xff08;CL&#xff09;与匹配电容&#xff08;CL1、CL2&#xff09;的关系 2. 如何选择 CL1 和 CL…...

MFE(微前端) Module Federation:Webpack.config.js文件中每个属性的含义解释

以Module Federation 插件详为例&#xff0c;Webpack.config.js它可能的配置和含义如下&#xff1a; 前言 Module Federation 的Webpack.config.js核心配置包括&#xff1a; name filename&#xff08;定义应用标识&#xff09; remotes&#xff08;引用远程模块&#xff0…...

【堆垛策略】设计方法

堆垛策略的设计是积木堆叠系统的核心&#xff0c;直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法&#xff0c;涵盖基础规则、优化算法和容错机制&#xff1a; 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则&#xff1a; 大尺寸/重量积木在下&#xf…...