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

【JavaEE初阶】网络编程

  

欢迎关注个人主页:逸狼


创造不易,可以点点赞吗~

如有错误,欢迎指出~



⽹络编程,指⽹络上的主机,通过不同的进程,以编程的⽅式实现⽹络通信(或称为⽹络数据传 输)。

socket api

Socket套接字,是由系统提供⽤于⽹络通信的技术,是基于TCP/IP协议的⽹络通信的基本操作单元。 基于Socket套接字的⽹络程序开发就是⽹络编程

操作系统给应用程序(传输层给应用层)提供的api,就叫做socket api(这里学习Java版本的),有两组不同的api,分别为UDP和TCP两套版本

  • UDP 无连接 不可靠传输 面向数据报 全双工
  • TCP 有连接 可靠传输 面向字节流 全双工

  • 有/无连接: 通信双方若保存了通信对端的信息(IP和端口),就是有连接,不保存就是无连接
  • 可靠/不可靠传输:尽可能考虑能够到达对方就是可靠传输,完全不考虑就是不可靠传输.这个在代码中没办法直接体现,它是在内核中实现好的功能
    • TCP内置了一些机制(感知到对方是否收到; 重传机制,在对方没收到时进行重试)可以保证可靠传输( 但是可靠传输要付出代价 ,TCP协议设计要比UDP复杂很多,也会损失一些传输数据的效率)
    • UDP没有可靠性机制 
  • 面向字节流/数据报 :参数单位是字节的就是面向字节流,单位是数据包的就是面向数据报 
    • TCP是面向字节流的,传输过程和文件流/水流是一样的特点
    • UDP是面向数据报的,传输数据的基本单位就是UDP数据报,一次发送/接收必须是完整的数据报
  • 全/半双工:一个通信链路,可以发送数据,也可以接收数据就是全双工; 只能发送或只能接收就是半双工  这里写的代码都是全双工的,不考虑半双工

UDP版本socket api

通过代码不好直接操作网卡(网卡有很多不同的型号,之间提供的api都会有差别),操作系统就把网卡概念封装成socket,应用程序员就不必关注硬件的差异和细节,统一操作socket对象就能间接操作网卡;  socket 可以认为是操作系统中广义文件下里的一种文件类型,这样的文件就是网卡这种硬件设备的抽象表现形式

代码部分需要实现两个程序

socket相当于网卡的遥控器,网络编程必须要操作网卡,就需要用到socket对象DatagramSocket

下面通过写一个"回显服务器(echo server)"(客户端发的请求和服务器返回的响应一致)代码示例加以理解

UDP服务器

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

public class UdpEchoServer {
    private DatagramSocket socket=null;

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    //通过start 启动服务器的核心流程
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            //通过死循环不停的处理客户端的请求

            //1.读取客户端的请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //上述收到的数据,是二进制byte[]的形式体现的,后续代码如果要进行打印之类的操作 需要将其转成字符串
            //构造string字符串            获取字节报的数据,从数组的0位置开始构造string,长度为字节报的长度
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());

            //2.根据请求计算响应,由于此处是回显服务器,响应就是请求
            String response = process(request);

            //3.把响应写回到客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
            ,requestPacket.getSocketAddress());//UDP是无连接的,所以要手动将客户端的请求的IP和端口号取出并包装到responsePacket里
            socket.send(responsePacket);

            //4.打印日志
            System.out.printf("[%s:%d] req=%s ,resp=%s\n",requestPacket.getAddress(),requestPacket.getPort()
            ,request,response);//获取IP地址和端口号,请求和响应
        }
    }

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

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

 

对于一个系统来说,同一时刻,同一个协议下,一个端口号,只能被一个进程绑定(端口号就是用来区分进程的,如果有多个进程尝试绑定一个端口号,后来的进程就会绑定失败),但是一个进程可以同时绑定多个端口号(通过创建多个socket对象来实现)   

比如:9090端口在udp下被一个进程绑定了,还可以在TCP下被另一个进程绑定

receive

 

DatagramSocket 这个对象中,不持有对方(客户端) 的 ip 和端口的. 所以进行 send 的时候,就需要在 send 的数据包里,把要发给谁这样的信息,写进去,才能够正确的把数据进行返回


socket在使用完之后需要关闭,此处代码中,socket生命周期整个进程一样长,就算没有close,进程关闭时也会释放文件描述符表里的所有内容,相当于close了

UDP客户端

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    public UdpEchoClient(String serverIP ,int serverPort) throws SocketException {
        socket =new DatagramSocket();
        this.serverIP=serverIP;
        this.serverPort =serverPort;
    }

    public void start() throws IOException {
        System.out.println("启动客户端!");
        Scanner scanner= new Scanner(System.in);

        while(true){
            //1.从控制台读取用户的输入
            System.out.print("-> ");
            String request = scanner.next();
            //2.构造出一个UDP请求,发送给服务器
            DatagramPacket requestPacket= new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(this.serverIP),this.serverPort);//这里要将IP字符串转换成int类型
            socket.send(requestPacket);
            //3.从服务器中读取响应
            DatagramPacket responsePacket= new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response= new String(responsePacket.getData(),0,requestPacket.getLength());
            //4.把响应打印到控制台上
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        //127.0.0.1称为环回IP,代表本机,如果服务器和客户端在同一个主机上,就使用这个IP
        UdpEchoClient udpEchoClient =new UdpEchoClient("127.0.0.1",9090);
        udpEchoClient.start();
    }
}

  • 服务器这边创建socket时一定要指定端口号,因为客户端是通过端口号来找到服务器的,且客户端是主动发起的一方.
  • 客户端这边创建socket时就最好不要指定端口号(不指定不代表没有,客户端的端口号是系统自动分配的一个端口,让系统自动分配一个端口,就能确保分配的是一个无人使用的端口).如果在客户端指定了端口号,由于客户端是在用户的电脑运行的,万一代码指定的端口和用户电脑上运行的其他程序的端口冲突,就会产生bug

服务器和客户端代码执行流程

TCP版本的socket api

TCP socket api核心类有两个

  1. ServerSocket  专门给服务器使用的socket对象
  2. Socket             给客户端使用,也可以给服务器使用

 TCP是有连接的,建立连接的过程类似于"打电话",ServerSocket类里的accept相当于"接电话"("客户端打电话,服务器接电话")

下面通过编写TCP回显服务器来举例展示

TCP服务器

package net;

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();//serverSocket用于帮助clientSocket建立连接
            //使用多线程来实现多个客户端连接服务器
            Thread t= new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
    //针对一个连接,提供处理逻辑
    private void processConnection(Socket clientSocket) throws IOException {
        //先打印一下客户端的信息
        System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort());
        //获取到socket中持有的流对象
        try(InputStream inputStream = clientSocket.getInputStream();//TCP是全双工的通信,一个socket对象既可以读也可以写
            OutputStream outputStream = clientSocket.getOutputStream()){
            //使用Scanner包装一下inputStream ,就可以更方便的读取这里的请求数据了
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true){
                //1.读取请求并解析
                if(!scanner.hasNext()){
                    //如果scanner无法读取出数据,说明客户端关闭了连接,导致服务器读到了"末尾"
                    break;
                }
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回客户端
                //此处可以按照字节数组直接写,也可以有另一种写法
//                outputStream.write(response.getBytes());
                printWriter.println(response);
                printWriter.flush();//刷新缓冲区

                //4.打印日志
                System.out.printf("[%s:%d] req=%s,resp=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{//只要是方法执行完毕了,就会执行close代码
            //连接失败打印日志
            System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
            clientSocket.close();//客户端socket需要手动关闭
        }
    }

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

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

约定换行符 

TCP客户端

package net;

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 TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //这里写入ip和端口号之后意味着new好对象之后 和服务器的连接就建立完成了
        //如果建立连接失败了,直接就会抛出异常
        socket = new Socket(serverIp,serverPort);
    }

    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();
                //4.打印响应结果
                System.out.println(response);
            }
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }

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

容易产生bug的点 

刷新缓冲区

PrintWrite这样的类以及很多IO流中的类都是"自带缓冲区的",引入缓冲区后,进行写数据操作不会立即触发IO,而是放到内存缓冲区中,等到缓冲区赞了一波,在进行统一发送;

所以当要发送的数据较少时 ,没办法攒够数据发送,停在了缓冲区,所以要引入PrintWrite类里的flush操作 来主动 "刷新缓冲区"

 clientSocket要自动关闭close

像ServerSocket,DatagramSocket他们的生命周期都是跟随整个进程的(进程结束,会自动关闭),而服务器代码中clientSocket是"连接级别"的数据,随着客户端断开连接,这个socket就不再使用了(即使是同一个客户端,断开之后,重新连接,也和旧的socket不是同一个),这样的socket应该主动关闭以防止 文件资源泄漏.

多个客户端连接同一个服务器

此处单线程下无法处理多个客户端本质是服务器代码里 双重while循环导致的,进入了里层的while循环时,外层while无法执行了 ,所以这里要比双层while循环改成一异while,分别执行-->使用多线程解决.主线程用于accept来获取多个连接 ,每个连接都可以启动一个新的线程

虽然创建线程比创建进程 更轻量,但是也架不住短时间内 ,创建销毁大量的线程 

使用线程池 可以解决短时间客户端涌入,并且每个客户端请求都很快 的问题

        System.out.println("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true){
            Socket clientSocket = serverSocket.accept();//serverSocket用于帮助clientSocket建立连接
            //使用多线程来实现多个客户端连接服务器
//            Thread t= new Thread(()->{
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();
            //使用线程池
            service.submit(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });

长短连接

  • 长连接: 客户端连上服务器后,一个连接中会多次发出请求,接收多个响应(当前回显服务器就属于这种模式)
  • 短连接: 客户端连上服务器后,一个连接只能发一个请求,接受一个响应,然后就断开连接了(可能会频繁和服务器建立/断开连接)

http://www.kler.cn/a/375321.html

相关文章:

  • 2024最新鸿蒙开发面试题合集(一)-HarmonyOS NEXT Release(API 12 Release)
  • 【ES6复习笔记】Class类(15)
  • Linux之ARM(MX6U)裸机篇----2.汇编LED驱动实验
  • HW护网分析研判思路,流量告警分析技巧
  • RabbitMQ中的Topic模式
  • 【零基础保姆级教程】制作自己的数据集(二)——Labelme的安装与使用及常见的报错解决方法
  • NOIP 2024北京市报名通知
  • GPU 服务器厂家:中国加速计算服务器市场的前瞻洞察
  • Spring Cloud Function快速入门Demo
  • 如何正确进行activemq服务搭建及性能调优?
  • flutter调用原生实现连接控制称重设备
  • vue下载安装
  • 安卓早期apk兼容性适配之内存读写
  • 自然语言处理方向学习建议
  • 一文带你了解:六款适合PC端的工时管理工具
  • 【Three.js】SpriteMaterial 加载图片泛白,和原图片不一致
  • 商家如何在高德地图上申请店铺入驻?
  • 使用libimobiledevice+ifuse访问iOS沙盒目录
  • SQL内外连接详解
  • sudo apt install 安装位置
  • 音视频入门基础:FLV专题(22)——FFmpeg源码中,获取FLV文件音频信息的实现(中)
  • 【大语言模型】ACL2024论文-05 GenTranslate: 大型语言模型是生成性多语种语音和机器翻译器
  • 基于SSM学生竞赛模拟系统的设计
  • 脉冲当量计算方法
  • 服务器对于企业业务有哪些影响?
  • 无头双向链表模拟实现