JavaEE初阶-网络初识-网络编程套接字

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

选择代码,Ctrl+Alt+t可以触发包围模式,快速捕获异常

1. 发展历史

在这里插入图片描述
在这里插入图片描述

中间的是交换机,最后那个是路由器

在这里插入图片描述
移动互联网就是手机
广域网,就是所有人构成的一个网络

2. 重要概念

2.1 IP地址

IP地址主要⽤于标识⽹络主机、其他⽹络设备(如路由器)的⽹络地址。简单说,IP地址⽤于定位主机的⽹络地址。

输入ipconfig-》可获取ip地址
在这里插入图片描述

2.2 端⼝号

在⽹络通信中,IP地址⽤于标识主机⽹络地址,端⼝号可以标识主机中发送数据、接收数据的进程。简单说:端⼝号⽤于定位主机中的进程
在这里插入图片描述

2.3 认识协议

协议,⽹络协议的简称,⽹络协议是⽹络通信(即⽹络数据传输)经过的所有⽹络设备都必须共同遵从的⼀组约定、规则。如怎么样建⽴连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。
协议(protocol)最终体现为在⽹络上传输的数据包的格式

计算机之间的传输媒介是光信号和电信号

2.4 协议分层

为什么需要⽹络协议的分层?
分层最⼤的好处,类似于⾯向接⼝编程:定义好两层间的接⼝规范,让双⽅遵循这个规范来对接。在代码中,类似于定义好⼀个接⼝,⼀⽅为接⼝的实现类(提供⽅,提供服务),⼀⽅为接⼝的使⽤类(使⽤⽅,使⽤服务):
• 对于使⽤⽅来说,并不关⼼提供⽅是如何实现的,只需要使⽤接⼝即可
• 对于提供⽅来说,利⽤封装的特性,隐藏了实现的细节,只需要开放接⼝即可。

2.5 OSI七层模型

OSI:即Open System Interconnection,开放系统互连
• OSI 七层⽹络模型是⼀个逻辑上的定义和规范:把⽹络从逻辑上分为了7层。
• OSI 七层模型是⼀种框架性的设计⽅法,其最主要的功能使就是帮助不同类型的主机实现数据传输;
在这里插入图片描述
在这里插入图片描述
OSI 七层模型既复杂⼜不实⽤:所以 OSI 七层模型没有落地、实现

2.6 TCP/IP五层(或四层)模型

TCP/IP是⼀组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。
TCP/IP通讯协议采⽤了5层的层级结构,每⼀层都呼叫它的下⼀层所提供的⽹络来完成⾃⼰的需求。
• 应⽤层:负责应⽤程序间沟通,如简单电⼦邮件传输(SMTP)、⽂件传输协议(FTP)、⽹络远程访问协议(Telnet)等。我们的⽹络编程主要就是针对应⽤层。
• 传输层:负责两台主机之间的数据传输。如传输控制协议 (TCP),能够确保数据可靠的从源主机发送到⽬标主机。
• ⽹络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识⼀台主机,并通过路由表的⽅式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)⼯作在⽹路层。
• 数据链路层:负责设备之间的数据帧的传送和识别。例如⽹卡设备的驱动、帧同步(就是说从⽹线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就⾃动重发)、数据差错校验等⼯作。有以太⽹、令牌环⽹,⽆线LAN等标准。交换机(Switch)⼯作在数据链路层。
• 物理层:负责光/电信号的传递⽅式。⽐如现在以太⽹通⽤的⽹线(双绞 线)、早期以太⽹采⽤的的同轴电缆(现在主要⽤于有线电视)、光纤,现在的wifi⽆线⽹使⽤电磁波等都属于物理层的概念。物理层的能⼒决定了最⼤传输速率、传输距离、抗⼲扰性等。集线器(Hub)⼯作在物理层。

在这里插入图片描述

在这里插入图片描述

应用层,就是决定把数据拿来干嘛的

在这里插入图片描述
物理层我们考虑的⽐较少。因此很多时候也可以称为 TCP/IP四层模型

⽹络设备所在分层
• 对于⼀台主机,它的操作系统内核实现了从传输层到物理层的内容,也即是TCP/IP五层模型的下四层;
• 对于⼀台路由器,它实现了从⽹络层到物理层,也即是TCP/IP五层模型的下三层;
• 对于⼀台交换机,它实现了从数据链路层到物理层,也即是TCP/IP五层模型的下两层;
• 对于集线器,它只实现了物理层;

注意我们这⾥说的是传统意义上的交换机和路由器,也称为⼆层交换机(⼯作在TCP/IP五层模型的下两层)、三层路由器(⼯作在TCP/IP五层模型的下三层)。
随着现在⽹络设备技术的不断发展,也出现了很多3层或4层交换机,4层路由器。我们以下说的⽹络设备都是传统意义上的交换机和路由器。

在这里插入图片描述
在这里插入图片描述

2.7 封装和分⽤

不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在⽹络层叫做数据报(datagram),在链路层叫做帧(frame)。
• 应⽤层数据通过协议栈发到⽹络上时,每层协议都要加上⼀个数据⾸部(header),称为封装(Encapsulation)。
• ⾸部信息中包含了⼀些类似于⾸部有多⻓,载荷(payload)有多⻓,上层协议是什么等信息。
• 数据封装成帧后发到传输介质上,到达⽬的主机后每层协议再剥掉相应的⾸部,根据⾸部中的 “上层协议字段” 将数据交给对应的上层协议处理

下图为数据封装的过程
在这里插入图片描述
下图为数据分⽤的过程
在这里插入图片描述

例子
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. ⽹络编程套接字

操作系统给应用程序(传输层到应用层)提供的API,就叫socket api(java版本)

UDP和TCP都有一套,socket api

UDP和TCP区别
TCP有连接,可靠传输,面向字节流,全双工
UDP:无连接,不可靠传输,面向数据报,全双工

有无连接:通信双方是否保存对端的信息,保存就是有连接,不保存对端的信息就是无连接
可靠传输:信息尽可能到达对方,有重传机制,对方没有收到就重传,更加复杂,效率低
不可靠传输:完全不考虑数据能否到达对方,到不到达不管的,没有重传
面向字节流:传输过程就和文件流是一样的特点
在这里插入图片描述

面向数据报:传输数据的基本单位不是字节,而是UDP数据报,一次发送接收,必须发送接收一个完整的UDP数据报

全双工:一个通信链路,可以发送数据,也可以接收数据,一个网线里面有8根铜线,可以全双工
半双工:一个通信链路,只能发送数据或者接收数据(单向通信)

4. UDP数据报套接字编程

4.1 DatagramSocket和DatagramPacket

DatagramSocket 是UDP Socket,⽤于发送和接收UDP数据报。代表一个socket对象,是操作系统下一种广义的文件类型,这种文件就是网卡这种硬件设备
代码不好直接操作网卡,操作系统把网卡概念分装为socket,这样就可以操作socket去操作网卡了
DatagramPacket代表一个UDP数据报,UDP传输的基本单位
DatagramPacket构造方法
DatagramSocket()创建⼀个UDP数据报套接字的Socket,绑定到本机任意⼀个随机端⼝(⼀般⽤于客⼾端)
DatagramSocket(int port) 创建⼀个UDP数据报套接字的Socket,绑定到本机指定的端⼝(⼀般⽤于服务端)
DatagramPacket方法
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该
⽅法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发
送)
void close() 关闭此数据报套接字
DatagramPacket构造方法
DatagramPacket(byte[] buf, int length) 构造⼀个DatagramPacket以⽤来接收数据报,接收的数据保存在字节数组(第⼀个参数buf)中,接收指定⻓度(第⼆个参数length)
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address)
构造⼀个DatagramPacket以⽤来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定⻓度(第⼆个参数length)。address指定⽬的主机的IP和端⼝号

DatagramPacket方法
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端⼝号;或从发送的数据报中,获取接收端主机端⼝号
byte[] getData() 获取数据报中的数据

代码实现需要两个程序,一个客户端,一个服务端,主动发起通信:客户端,被动接受:服务端

4.2 回显服务器

客户端发啥,服务器就返回啥

public class UDPServer {
    private DatagramSocket socket = null;

    public UDPServer(int port) throws SocketException {
        //服务器socket需要指定端口号,这样客户端才能找到--->进程和api端口号绑定了
//        同一时刻,一个端口号只能被一个进程绑定,但是一个进程可以绑定多个端口号,创建多个socket,多个进程不能绑定同一个端口号
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        //启动之后,不停处理客户端发来的请求,随时准备好,客户端来消息--》处理
        while (true){
            //需要指定DatagramPacket需要存储数据的字节数组和数组长度
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //1,读取客户端的请求并解析,receive从网卡上的对应端口号读取数据,没有数据的话,就会阻塞等待
//            也是通过一个输出型参数DatagramPacket来获取网卡上的数据的
            socket.receive(requestPacket);
//            上述收到的数据都是二进制byte[]的形式--->转为String,getData获取字节数组,getLength是数据包的长度
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
            //2,根据请求,计算响应,
            String response = process(request);
            //3. 返回给客户端,responsePacket必须是有内容的,不能为空的字节数组了,长度不能写response.length,因为response的是字符个数,编码方方式要注意
            //都是英文字母---》没事,有中文字符,utf8---》有问题了,一个中文,三个字节
            //因为UDP无连接,所以DatagramSocket不持有对方的ip和端口号,所以发送的时候DatagramPacket还要指定端口号和ip
            //requestPacket.getSocketAddress()从数据包中就可以得到对方的ip和端口号了
            DatagramPacket responsePacket = new DatagramPacket
                    (response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印日志
            System.out.printf("[%s:%d],req=%s,res=%s \n",requestPacket.getAddress(),requestPacket.getPort(),request,response);
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UDPServer server = new UDPServer(9090);
        server.start();
    }

}

socket这个文件也是需要关闭的—》但是这里是进程结束了,socket才关闭

public class UDPClient {
    DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    public UDPClient(String serverIP,int ServerPort) throws SocketException {
        socket = new DatagramSocket();//没有指定端口号---》默认一个随机的无人使用的端口号,因为服务器是被动接受,所以服务器要指定端口号才找得到
        this.serverIP = serverIP;//这是服务器的ip和端口号
        this.serverPort = ServerPort;
    }

    public void start() throws IOException {
        System.out.println("启动客户端");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            //1.从用户读取用户的输入
            System.out.println("-> ");
            String request = scanner.next();
            //构造UDP请求,发送DatagramPacket中带有目标的ip和端口号
//            DatagramPacket packet = new DatagramPacket(request.getBytes(), request.getBytes().length,this.serverIP,this.serverPort);
            //这个构造DatagramPacket不支持,因为我们这里输入的ip是点分十进制的ip,但是它需要的ip是一个整数,所以要转换一下InetAddress.getByName
            DatagramPacket packet = new DatagramPacket
                    (request.getBytes(), request.getBytes().length, InetAddress.getByName(this.serverIP),this.serverPort);
            socket.send(packet);
            /// 3.从服务器读取响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //4.把响应打印到控制台上
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UDPClient udpClient = new UDPClient("127.0.0.1",9090);
        udpClient.start();
    }
}

先启动服务器,在启动客户端

在这里插入图片描述
在这里插入图片描述

            String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");

这里可以指定编码,默认就是utf8,所以打印出来的中文是正常的

文件那里,字节流,(char)c---->不能获取正常中文,因为utf8是三个字节,这里值转换了一个字节,所以如果要正常显示的话,也必须指定编码为utf8
字符流的话—》把utf8的三个字节弄为一个字节了–》所以(char)c能正常获取到中文

socket.receive(requestPacket);

每次都会在这里阻塞等待

如果要跨主机的话-----》必须连同一个wifi才可以–》同一个局域网
或者服务器代码放在云服务器上–》访问公网ip

//实现一个英译汉
public class UdpDictServer extends UDPServer{
    private Map<String,String> dict= new HashMap<String,String>();
    public UdpDictServer(int port) throws SocketException {
        super(port);
        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("pig","小猪");
    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request,"未知单词");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer udpDictServer = new UdpDictServer(9090);
        udpDictServer.start();
    }
}

在这里插入图片描述

在这里插入图片描述

4.3 端口号被占用

netstat -ano

可以看到所有端口号的进程和进程id

netstat -ano | findstr 9090

这里可以查看指定的端口号得到进程

netstat -ano | grep 9090

linux上就是grep了
同一个协议下,一个端口号只能被一个进程绑定
9090端口,在UDP下被一个进程绑定了,还可以在TCP下被另一个进程绑定

5. TCP流套接字编程

5.1 ServerSocket和Socket

ServerSocket:专门给服务器使用
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造⽅法:

Socket
Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据的。

这里没有TCP数据包,这种基本单位,TCP是面向字节流的,传输基本单位就是byte

UDP是面向数据报:所以需要定义专门的类,表示UDP数据报,作为UDP传输的基本单位

所以ServerSocket和Socket都是socket

ServerSocket 构造⽅法:
ServerSocket(int port) 创建⼀个服务端流套接字Socket,并绑定到指定端⼝

ServerSocket ⽅法:

Socket accept() 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该Socket建⽴与客⼾端的连接,否则阻塞等待
void close() 关闭此套接字

Socket 构造⽅法:
Socket(String host, int port) 创建⼀个客⼾端流套接字Socket,并与对应IP的主机
上,对应端⼝的进程建⽴连接,构造这个对象,就直接去建立连接了
Socket ⽅法:
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输⼊流,这是字节流
OutputStream getOutputStream() 返回此套接字的输出流

5.2 回显服务器

public class TCPServer {
    private ServerSocket serverSocket=null;
    public TCPServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            Socket clientSocket = serverSocket.accept();//阻塞等待客户端建立连接,并返回客户端socket一直循环,一直建立很多连接
            processConnection(clientSocket);//其中一个连接的处理
        }
    }

    //针对一个连接,提供处理逻辑
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s,%d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //获取socket中持有的流对象
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream();
            PrintWriter printWriter = new PrintWriter(outputStream)) {
            //tcp是全双工的,既可以读也可以写
            //使用Scanner来包装InputStream
            while (true) {
                // 1.读取请求并解析
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    //如果scanner中无法读取数据,说明客户端关闭了连接了,因为scanner会一直阻塞等待客户端的输入的,hasNext说明客户端ctrl+c了和C语言一样
                    break;
                }
                String request = scanner.next();//请求应该是以空白符,换行符等结尾
                //scanner.nextLine()要求必须带有\n
                //2.根据请求计算响应
                String response   = process(request);
                //3.返回客户端
//                outputStream.write(response.getBytes());//用字节数组的形式来写入
                //println在写入响应的时候会换行---》客户端收到的时候scanner.next(),就会知道这次输入结束了,不然会一直等着什么时候服务端返回结束
                printWriter.println(response);
                //4.打印日志
                System.out.printf("[%s,%d],req=%s,res=%s\n",
                        clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        System.out.printf("[%s,%d]客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());

    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TCPServer tcpServer = new TCPServer(9090);
        tcpServer.start();
    }

}
public class TCPClient {
    private Socket socket=null;
    public  TCPClient(String serverIp,int ServerPort) throws IOException {
        socket = new Socket(serverIp,ServerPort);//要指定服务器的ip和端口,一旦new好,就会和服务器建立连接,而且可以直接填写Stringip,不用转换
    }
    public  void start(){
        System.out.println("客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream())   {
            Scanner scanner = new Scanner(inputStream);
            Scanner scannerIn = new Scanner(System.in) ;
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                //1.从控制台读取数据
                System.out.print("->");
                String request = scannerIn.next();
                //2.把请求发送到服务器
                printWriter.println(request);
                //3.从服务器读取响应
                if (!scanner.hasNext()){
                    break;
                }
                String response = scanner.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TCPClient tcpClient =new TCPClient("127.0.0.1",9090);
        tcpClient.start();
    }


}
if (!scanner.hasNext()){

这里会阻塞:等待客户端发送数据,等待客户端println
在客户端发数据之前,hasNext是不会返回值的,会一直阻塞,没有HasNext的话,Next也是会阻塞的。除非客户端主动退出—》HasNext才会返回

serverSocket.accept()

这里也会阻塞

在这里插入图片描述
在这里插入图片描述

然后进行测试
客户端虽然建立了连接,但是客户端发送数据之后没有响应—》客户端没有发出去请求

printWriter.println(response);

像PrintWriter这种类,以及很多IO流中的泪,都是自带缓冲区的
引入缓冲区之后,进行写入数据的操作,不会立即出发IO,而是会先放在内存缓冲区中,等缓冲区放了很多之后,在统一进行发送
缓冲区不好放满,因为我们的数据太少了–》刷新缓冲区
flush操作

                printWriter.println(response);
                printWriter.flush();

改为这样就可以了

然后客户端和服务器都要加flush

在这里插入图片描述
在这里插入图片描述
这样就成功了

            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);

还有要注意这里,UDP的socket的生命周期是跟随整个进程的,进程没了,它就没了
但是TCP的clientSocket 这个,客户端断开连接了,这个socket就不在使用了,即使是同一个客户端重新连接–》也是一个新的socket----》这个在服务器上的客户端的socket是不会随着客户端的关闭,然后服务器上的clientSocket 是自动关闭的—》文件资源泄露–》所以我们要手动关闭

        }finally {
            System.out.printf("[%s,%d]客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
            clientSocket.close();
        }

这个是一定会执行的

还有一个问题就是一个服务器怎么同时给多个客户端提供服务呢
我们的server可以吗,我们的不可以,因为是但线程的,我们实现的这个start是单线程的,每次都只有一个才能连接成功,只有这个连接没了,才能退出processConnection,退出循环,才能到accept等待下一个客户端连接

首先怎么启动多个客户端呢

在这里插入图片描述

勾选允许多个实例

这样点击运行,就是自动运行多个实例了

在这里插入图片描述

但是客户端上线只有一个,所以不能多个连接的
但是我们关闭一个客户端,另一个客户端就可以连接了,因为另一个客户端一直等着连接呢

    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            Socket clientSocket = serverSocket.accept();//阻塞等待客户端建立连接,并返回客户端socket一直循环,一直建立很多连接
            Thread thread = new Thread(()->{
                try {
                    processConnection(clientSocket);//其中一个连接的处理
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();
        }
    }

这样就可以了

在这里插入图片描述
这样就可以了
还可以优化–》线程池,万一突然来了很多客户端呢,然后连一会儿就断开了,后面又要重新创建线程

            ExecutorService executorService = Executors.newCachedThreadPool();
            //使用线程池
            executorService.submit(()->{
                try {
                    processConnection(clientSocket);//其中一个连接的处理
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });

newCachedThreadPool的最大线程数是一个很大的数

但是万一来了几百万个客户端呢–》几百万个线程–》机器不行了—》使用IO多路复用
IO多路复用就是在网络服务器开发得到时候,使用很少的线程处理很多的客户端
一个线程可以给多个客户端提供服务(因为一个客户端要阻塞,所以一个线程搞得赢处理多个)

        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream())   {
            Scanner scanner = new Scanner(inputStream);
            Scanner scannerIn = new Scanner(System.in) ;
            PrintWriter printWriter = new PrintWriter(outputStream);

scanner 和printWriter 要close吗,这个没有放在try里面啊
可以不放在try里面,因为它们就是分装的inputStream 和outputStream,文件描述符就是inputStream 和outputStream持有的

或者scanner 关闭也可以,它关闭也会顺便关闭inputStream
Scanner scannerIn = new Scanner(System.in) ;这个要关闭吗
一个进行中有三个特殊流对象特殊的文件描述符是不需要关闭的
这三个是进程一启动,操作系统自动打开的,生命周期跟随整个进程

System.in,System.out,System.err
就是这三个
在这里插入图片描述
在这里插入图片描述
完整代码

public class TCPClient {
    private Socket socket=null;
    public  TCPClient(String serverIp,int ServerPort) throws IOException {
        socket = new Socket(serverIp,ServerPort);//要指定服务器的ip和端口,一旦new好,就会和服务器建立连接,而且可以直接填写Stringip,不用转换
    }
    public  void start(){
        System.out.println("客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream())   {
            Scanner scanner = new Scanner(inputStream);
            Scanner scannerIn = new Scanner(System.in) ;
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                //1.从控制台读取数据
                System.out.print("->");
                String request = scannerIn.next();
                //2.把请求发送到服务器
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应
                if (!scanner.hasNext()){
                    break;
                }
                String response = scanner.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TCPClient tcpClient =new TCPClient("127.0.0.1",9090);
        tcpClient.start();
    }


}
public class TCPServer {
    private ServerSocket serverSocket=null;
    public TCPServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            Socket clientSocket = serverSocket.accept();//阻塞等待客户端建立连接,并返回客户端socket一直循环,一直建立很多连接
            ExecutorService executorService = Executors.newCachedThreadPool();
//            Thread thread = new Thread(()->{
//                try {
//                    processConnection(clientSocket);//其中一个连接的处理
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
//            });
//            thread.start();
            //使用线程池
            executorService.submit(()->{
                try {
                    processConnection(clientSocket);//其中一个连接的处理
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

    //针对一个连接,提供处理逻辑
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s,%d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //获取socket中持有的流对象
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream();
            PrintWriter printWriter = new PrintWriter(outputStream)) {
            //tcp是全双工的,既可以读也可以写
            //使用Scanner来包装InputStream
            while (true) {
                // 1.读取请求并解析
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    //如果scanner中无法读取数据,说明客户端关闭了连接了,因为scanner会一直阻塞等待客户端的输入的,hasNext说明客户端ctrl+c了和C语言一样
                    break;
                }
                String request = scanner.next();//请求应该是以空白符,换行符等结尾
                //scanner.nextLine()要求必须带有\n
                //2.根据请求计算响应
                String response   = process(request);
                //3.返回客户端
//                outputStream.write(response.getBytes());//用字节数组的形式来写入
                //println在写入响应的时候会换行---》客户端收到的时候scanner.next(),就会知道这次输入结束了,不然会一直等着什么时候服务端返回结束
                printWriter.println(response);
                printWriter.flush();
                //4.打印日志
                System.out.printf("[%s,%d],req=%s,res=%s\n",
                        clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            System.out.printf("[%s,%d]客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
            clientSocket.close();
        }

    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TCPServer tcpServer = new TCPServer(9090);
        tcpServer.start();
    }

}

6. ⻓短连接

TCP发送数据时,需要先建⽴连接,什么时候关闭连接就决定是短连接还是⻓连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能⼀次收发数据。
⻓连接:不关闭连接,⼀直保持连接状态,双⽅不停的收发数据,即是⻓连接。也就是说,⻓连接可以多次收发数据。

对⽐以上⻓短连接,两者区别如下:
• 建⽴连接、关闭连接的耗时:短连接每次请求、响应都需要建⽴连接,关闭连接;⽽⻓连接只需要第⼀次建⽴连接,之后的请求、响应都可以直接传输。相对来说建⽴连接,关闭连接也是要耗时的,⻓连接效率更⾼。
• 主动发送请求不同:短连接⼀般是客⼾端主动向服务端发送请求;⽽⻓连接可以是客⼾端主动发送请求,也可以是服务端主动发。
• 两者的使⽤场景有不同:短连接适⽤于客⼾端请求频率不⾼的场景,如浏览⽹⻚等。⻓连接适⽤于客⼾端与服务端通信频繁的场景,如聊天室,实时游戏等

短连接:频繁建立连接----》用得少了

总结

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值