一、
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配合使用。