一、💛
TCP分量比UDP更重,协议更多,字节流,一个字节一个字节传输,一个TCP数据报就是一个字节数组,byte[](也就是说不用整我们那个文件报)
主要分为两个类:
ServerSocket:给服务器使用的Socket。
Socket:既会给服务器用,也会给客户端使用
“连接”连接其实更准确的是说,服务器与客户端建立绑定关系,互相保存对方信息。
握手是系统的内核负责->一个服务器,要对应很多客户端,服务器内核里面有很多客户端连接->虽然内核中的连接很多,但是在应用程序中,还是要一个一个处理的~~
内核中的“连接”就像一个一个的“待办事项”,这些待办事项在一个队列数据结构中, 应用程序就需要一个一个完成这些任务~~
要完成任务,也就需要先取任务:
serverSocket.accept()。->用户执行accept的时候,此时客户端还没来呢,所以会进行阻塞,直到有客户端连接成功为止。
二、 💙
把内核中的连接获取到应用程序,过程类似于“生产者,消费者模型”
客户端和服务器建立连接的时候,服务器就会和用户(客户端)进行一系列的数据交互,成为“握手”,这个过程建立完成了之后,连接就建立完成了,此时一个连接就会产生一个元素,消费的时候取出元素,放到应用程序里面处理,这也就构成了生产者,消费者模型
三、💜
一次IO操作,主要分为两个部分:
1.等(阻塞)
2.拷贝数据
介绍部分服务器核心代码
//accept是把内核中已经建立好的连接,给拿到应用程序中, //但是这里的返回值并非是一个Connection这样的对象, //而只是一个Socket对象,后续是Socket进行操作,换句话说:你要买房 //这个accept()相当于路边的销售小哥,给你拉进屋子不管你了, //那个金牌销售小姐姐(Socket)给你服务 Socket clientSocket=serverSocket.accept();
1.Socket对象,相当于一个耳麦,可以对他说话,也可以听到声音,通过Socket对象和对方通信~,不用管对方到底是什么人,冲这个耳麦说话就行。
//inputStream相当于一个耳机,用来接收信息 //outputStream相当于一个麦克风,用来输出信息 try(InputStream inputStream=clientSocket.getInputStream(); OutputStream outputStream=clientSocket.getOutputStream()){····}
2.String request=scanner.next();
next是读取数据,一直读到空白符号才结束
空白符包括但不限于:换行(\n让光标另起一行),回车(\r让光标回到行首),空格,制表符,翻页符,垂直制表符
3.flush()
PrintWrite writer=new PrintWriter(outputStream); //flush()刷新的意思,就相当于冲马桶,一下子全下去了 writer.flush()
IO 操作是比较有开销的,可用于访问内存,进行IO次数越多,程序速度越慢,使用一块内存作为缓存区,写数据的时候,先到缓冲区里,攒一波数据,统一进行IO,PrintWriter内置了缓冲区,所以手动刷新,确保这里的数据真正的全部通过网卡发出去了,而不是残留在内存缓冲中的(瞬间有味道了哈哈哈💨💨💨)
加flush()属于是确保稳定性,不加也不一定出错,缓存区内置了一定的刷新策略~比如缓冲区满了,就会触发刷新,再比如程序退出,也会触发刷新,推荐大家加上flush()
4.
🐷全缓冲:(往网课/文件里面写),一般是全缓冲,不会收到ln的影响
🐷行缓冲:(特殊情况,换行会刷新)一般在标准输入输出这里~(往控制台打印),控制台中,用户输入一条指令,都是用enter按钮进行确认的,enter本质是\n(Linux中),\r\n(windows)
5.
这个代码暗含一个约定,客户端发来的请求,得到的文本数据,同时还得是带有空白符号进行分隔(比如换行)
String request = scanner.next();是什么意思呢,代码演示一下,也就是这样,我们正常输入I love you 连一起的输入,他会当成是我们先发一个I,然后进行回车,再发送love,再回车,返回你love,你再发送you,返回你you
四、❤️
具体实现服务器,底下的只是大部分的实现逻辑,现在,会不会有小可爱们看出哪里有问题呢
import sun.nio.ch.sctp.SctpNet; 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 TcpEchoServer { //建立一个ServerSocket对象,构造方法去设置他的端口号 private ServerSocket serverSocket=null; public TcpEchoServer(int port) throws IOException{ serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动"); while (true){ //会停在这里,知道客户端上线, Socket clientSocket=serverSocket.accept(); //具体的操作方法写到这个方法之中 processConnection(clientSocket); } } private void processConnection(Socket clientSocket) throws IOException { //getInetAddress()是获取当前与Socket对象关联的InetAddress对象的字符串表示,在Socket类中getInetAddress()方法返回与(Socket)连接的远程对象,调用完这个方法之后,可以进一步调用toString()方法来获取改InetAddress对象的字符串表示。 //getPort()是返回与Socket连接的远程端口号。 System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); //接下来就可以读取请求,根据请求计算响应,返回响应,然后进行老三步 //Socket对象内部包含了两个字节流对象,可以把这俩字节流获取到,完成后,续写读写操作。 try(InputStream inputStream=clientSocket.getInputStream(); OutputStream outputStream=clientSocket.getOutputStream()) { //一次连接中,可能涉及到多次请求/响应 while (true) { //1.读取请求并且解析,为了读取方便,使用Scanner Scanner scanner = new Scanner(inputStream); //在客户端,没有发送请求的时候,也会进行阻塞,一直会阻塞到客户端真正发送了数据,或者客户端退出,hasNext()就返回了。 if (!scanner.hasNext()) { //1.读取完毕,客户端下线 System.out.printf("[%s:%d]客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } //这个代码暗含一个约定,客户端发来的请求,得到的文本数据,同时还得是带有空白符号进行分隔(比如换行) String request = scanner.next(); //2.根据请求计算响应 String response = process(request); //3.把响应写回客户端,把OutputStream使用PrinterWriter包裹一下,方便进行发数据 PrintWriter writer = new PrintWriter(outputStream); //使用PrintWriter的println方法,把响应返回给客户端。 //此处用println,而不是print就是为了在结尾加上\n,方便客户端读取响应,用Scanner.next()读取 writer.println(response); //这里还需要加上"刷新缓冲区操作" writer.flush(); //日志,打印当前的请求详情 //getInetAddress()是获取当前与Socket对象关联的InetAddress对象的字符串表示,在Socket类中getInetAddress()方法返回与(Socket)连接的远程对象,调用完这个方法之后,可以进一步调用toString()方法来获取改InetAddress对象的字符串表示。request表示你的请求,response表示返回服务器的请求 System.out.printf("[%s:%d] req:%s,resp:%s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); } } } public String process(String request){ return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer=new TcpEchoServer(9090); tcpEchoServer.start(); } }
好了我要公布答案了哦——
没错就是我们的关闭操作,因为Socket对象持有的文件描述符和之前那个一样,是需要关闭的,如果不关闭,就会这个文件描述符一直保持打开,关闭Socket对象的主要原因——避免资源泄露和过度占用。(带走你的年终奖大法)下面是改正
import sun.nio.ch.sctp.SctpNet; 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 TcpEchoServer { private ServerSocket serverSocket=null; public TcpEchoServer(int port) throws IOException{ serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动"); while (true){ Socket clientSocket=serverSocket.accept(); processConnection(clientSocket); } } private void processConnection(Socket clientSocket) throws IOException { System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); //接下来就可以读取请求,根据请求计算响应,返回响应,然后进行老三步 //Socket对象内部包含了两个字节流对象,可以把这俩字节流获取到,完成后,续写读写操作。 try(InputStream inputStream=clientSocket.getInputStream(); OutputStream outputStream=clientSocket.getOutputStream()) { //一次连接中,可能涉及到多次请求/响应 while (true) { //1.读取请求并且解析,为了读取方便,使用Scanner Scanner scanner = new Scanner(inputStream); if (!scanner.hasNext()) { //1.读取完毕,客户端下线 System.out.printf("[%s:%d]客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } //这个代码暗含一个约定,客户端发来的请求,得到的文本数据,同时还得是带有空白符号进行分隔(比如换行) String request = scanner.next(); //2.根据请求计算响应 String response = process(request); //3.把响应写回客户端,把OutputStream使用PrinterWriter包裹一下,方便进行发数据 PrintWriter writer = new PrintWriter(outputStream); //使用PrintWriter的println方法,把响应返回给客户端。 //此处用println,而不是print就是为了在结尾加上\n,方便客户端读取响应,用Scanner.next()读取 writer.println(response); //这里还需要加上"刷新缓冲区操作" writer.flush(); //日志,打印当前的请求详情 System.out.printf("[%s:%d] req:%s,resp:%s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); } } catch (IOException e){ e.printStackTrace(); }finally { //在finally中加上close操作,确保当前socket被及时关闭 clientSocket.close(); } } public String process(String request){ return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer=new TcpEchoServer(9090); tcpEchoServer.start(); } }
五、💚
下面来几个问题理解一手
当服务器接收到客户端的请求并对其进行处理后,可以通过获取一个PrintWriter对象,并使用它的print或者println方法将响应文本写入输出流。该输出流最终会传输到客户端,客户端可以读取并处理这些响应程序
🐣🐣客户端和服务器的inputStream和OutputStream是四个文件在接收传输数据吗?
答:准确的说是四个流对象,实际上是两个流对象。,通常情况下,是一个流对应一个文件的,这里是一个socket对象(文件)对应两个流对象。
写到try()里面是能自动关闭的,但是try{}这个里面就需要手动关闭,try with resources语法。
🐔🐔Scanner和PrintWriter是否进行close()呢,会不会有文件资源泄露呢?——不会!
流对象持有的资源,分为两个部分
1.内存(对象销毁,内存就回收了)
while循环一圈,内存自然销毁,scanner和printWriter没有文件描述符,持有的是inputStream,OutputStream引用(我们用try()已经关闭了)更准确的说持有文件描述符的是Socket对象。
2.文件描述符
六、 💖
具体去实现客户端代码
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 TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp, int serverPort) throws IOException { //这个new操作完成之后,就完成了tcp连接的建立 socket = new Socket(serverIp, serverPort); } public void start() throws IOException { System.out.println("客户端启动"); Scanner scannerConsole = new Scanner(System.in); //Socket对象内部包含两个内部字节流对象,可以把这两个字节流获取到,完成后续的读写操作。 try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { while (true) { //1.从控制台输入字符串 System.out.println("->"); //所有的next()的意思,都是一个空格,一个空格或者回车的这么读,比如你输入 //I love you ,他就会先输出I,在love 再you,他不会你再次输入,而是读取整个后存起来,一个一个一个读 String request = scannerConsole.next(); //2.把请求发送给服务器 PrintWriter printWriter = new PrintWriter(outputStream); //使用println带上换行,后续服务器读取请求,就可以用scanner.next来获取 printWriter.println(request); printWriter.flush(); //3.从服务器读取响应,也就是读取返回值 Scanner scannerNetWork = new Scanner(inputStream); //注意这里的next()的意思是,一块一块打印,而不是一整块打印,所以也需要while String response = scannerNetWork.next(); //4.把响应打印出来 System.out.println(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(); } }
但是他这样也有问题,当我们选择再开一个客户端的时候,发生了问题,第一个客户端好用,但是第二个客户端陷入阻塞,此时我关闭第一个客户端,第二个客户端变成第一个客户端,他又好使了。
这就说明我们的代码出现了bug,发现是我们的代码结构出现了问题。
//服务器的代码中你接收到一个客户端的请求,那么此时线程进行这个方法中, //那么你这个线程方法还没有结束,我们的第二个客户端该怎么启动呢, //你买房,那个销售小哥,给你拉过来,再给你讲房子如何如何,那么第二个客人该怎么拉过来呢? public void start() throws IOException { System.out.println("服务器启动"); while (true){ Socket clientSocket=serverSocket.accept(); processConnection(clientSocket); } }
此时破局之道在哪呢——看我那个比喻是不是感觉人像是不够用,感觉来个销售的小姐姐,那不是美滋滋 💖 💖 💖,这就需要用到我们之前的多线程了,这样两个客户端的问题就解决了。
//只需要改动服务器的这里就行,但是你也看到了这里 //我们进行频繁的创建线程。会造成一个资源的占用。 public void start() throws IOException { System.out.println("服务器启动"); while (true){ Socket clientSocket=serverSocket.accept(); Thread t=new Thread(()->{ try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } }); } }
所以改进方法:使用我们的线程池
public class TcpEchoServer { private ServerSocket serverSocket=null; //注意这里的线程池,不应该创建固定的数目 private ExecutorService service= Executors.newCachedThreadPool(); public TcpEchoServer(int port) throws IOException{ serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动"); while (true){ Socket clientSocket=serverSocket.accept(); //使用线程池创建问题 service.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }); } }
这时候我们来想一下第一个问题:关闭的问题,我在使用的时候用 try with resources OK不?
//此时这么写这个代码有错误,processConnection和主线程是不同的线程了 //此时你这么写,在执行processCOnnection过程中,主线程try 就执行完毕了. //这就会导致,clientSocket还没用完,我就先关闭了 //因此要把clientSocket交给ProcessConnection关闭 try(Socket clientSocket=serverSocket.accept()){ //使用线程池创建问题 service.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }
虽然这里使用线程,避免了频繁的创建线程,但毕竟每个客户端对应一个线程,如果服务器对应客户端很多,服务器就要创建出大量的线程,同时对服务器开销很大。
当客户端进一步增加的时候,线程数目进一步增加,系统负担越来越重,响应速度也会大打折扣。
七、⭐️
是否有办法,使用一个线程,高效处理很多客户端的并发(几万个那种)->(真正意义的高并发)
C10K:同一时刻有10k个客户端(即1w个),通过前面的一些手段和各种硬件设备,可以处理线程池类。
C10M:同时刻,有1kw的客户端并发(如某某🦌恋情),引入了很多的技术手段,其中一个必要手段之一,IO多路复用/IO多路转接(四个大字核心)
开源:引入更多的硬件设备
节流:提高单位硬件资源能够处理的请求数目。
IO多路复用——属于节流方式
例如:一家人不同的口味 饺子,包子,凉皮~~(人生不能缺少凉皮🌚🌚🌚)
我自己买,然后去别的地方买,然后再买,然后再等待,看哪个好,一起拎回去。
但是也可以同时出动一家人,他们三个去买,然后一起回来,这个相当于用了三个线程,可实际上,我一个线程不会比这三个线程慢多少。——操作系统提供的API,可搭配TCP,UDP API配合使用。