一)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){ //对于每一个客户端来说,服务器都会返回一个clientSocket Socket 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() { @Override public 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多路复用,咱们的系统同一时刻处理的线程数仍然是有限的,只是节省了一个客户端对应的资源,但是随着客户端的增加,还是会消耗更多的资源,我们就使用更多的硬件资源,此时每一台主机承受的压力就小了
以上三种都是一个基本处理高并发场景的,所使用的方法;