Java Web 实战 15 - 计算机网络之网络编程套接字

Posted 加勒比海涛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Web 实战 15 - 计算机网络之网络编程套接字相关的知识,希望对你有一定的参考价值。

文章目录

大家好 , 这篇文章给大家带来的是网络编程中的套接字 , 我们会着重讲解 socket 的套接字 , 包括了 UDP 的 Socket API 和 TCP 的 Socket API , 干货十足
推荐大家跳转到此链接查看文章
上一篇文章的链接也给大家贴在这里了
文章专栏在此

一 . 网络编程中的基本概念

1.1 网络编程

写代码 , 实现 两个/多个 进程 , 通过网络 , 来进行相互通信

我们之前介绍过 : 进程具有隔离性 (每个进程都有自己独立的虚拟地址空间)
进程间通信 , 借助每一个进程都能访问到的公共区域 , 完成数据交换
网络编程 , 也是一种进程间通信的方式 , 借助的公共区域就是网卡 , 这是当下最主流的方式 , 既能够让同一个主机的多个进程间通信 , 也可以让不同主机的多个进程间通信

1.2 客户端(client) / 服务器(server)

客户端 : 主动发送网络数据的一方
服务器 : 被动接受网络数据的一方

因为服务器无法知道客户端什么时候发送来资源 , 因此就只能长时间运行 , 甚至 7*24 小时运行

1.3 请求(request) / 响应(response)

请求 : 客户端给服务器发送的数据.
响应 : 服务器给客户端返回的数据.

1.4 客户端和服务器之间的交互数据

1.4.1 一问一答

客户端给服务器发个请求 , 服务器给客户端返回个响应
这是最常见的方式 , 比如 : 浏览网页

1.4.2 多问一答

客户端发多个请求 , 服务器返回一个响应
更少见一些 , 比如上传文件

1.4.3 一问多答

客户端发一个请求 , 服务器返回多个响应
还比较常见 , 比如下载文件

1.4.4 多问多答

客户端发送多个请求 , 服务器返回多个响应
比如 : 远程控制、游戏串流

所谓“串流游戏”,实际上是指通过有线或无线 WiFi 网络,将 PC / macOS / Linux 电脑上的游戏画面 实时“远程投射” 到另外一台设备屏幕上,如 iPhone、iPad、安卓手机、平板、智能电视或笔记本上,并进行远程游

二 . socket 套接字

进行网络编程 , 需要使用操作系统提供的网络编程 API

在传输层提供了两个非常重要且截然不同的协议 : TCP 和 UDP
这两个协议对应的 socket api 也是完全不同的
先简单的给大家总结一下 TCP 和 UDP
TCP : 有连接 , 可靠传输 , 面向字节流 , 全双工
UDP : 无连接 , 不可靠传输 , 面向数据报 , 全双工

有连接 : 打电话 -> 先建立连接 , 然后再通信
无连接 : 发微信 -> 不必建立连接 , 直接通信即可

网络通信无法保证 100% 到达的 (最糟糕的情况 : 网线被修狗咬断了)
可靠传输 : 数据 对方收没收到 , 发送方能够有感知
不可靠传输 : 数据 对方收没收到 , 发送方啥也不知道
打电话就相当于可靠传输 , 发微信就相当于不可靠传输

面向字节流 : 这里的字节流和文件那里的字节流是一样的 , 像水流一样 , 想传多少就传多少
面向数据报 : 以数据报作为传输的基本单位

全双工 : 双向通信 , 一个管道 , 能够 A -> B , 同时也能 B -> A (同时进行)
半双工 : 单向通信 , 一个管道 , 同一时刻 , 只能够 A -> B , 要么 B -> A (合走各的)

类似 , 网络通信主要借助网线来进行通信 . 一个标准的以太网线 , 里面其实是 8 根铜线 , 就分成了两条路 : 4 条线用来上传 , 4 条路用来下载

2.1 UDP 的 Socket API

2.1.1 引子

在 UDP 的 Socket API 中 , 有两个核心类 :
DatagramSocket :
叫做 socket 类 , 本质上相当于是一个 “文件” . 在系统中 , 还有一种特殊的 socket 文件 , 对应到网卡设备 .
要想进行网络通信 , 就需要先打开 socket 文件 . 我们构造一个 DatagramSocket 对象 , 就相当于打开了一个内核中的 socket 文件 .
打开之后 , 就可以传输数据了
传输数据通过两个方法 : send(发送数据)、receive(接受数据)
还有一个 close 操作关闭文件

DatagramPacket :
表示一个 UDP 数据报 , UDP 是面向数据报的协议 , 传输数据 , 就是以 DatagramPacket 为基本单位

还有一个非常重要的类 : InetSocketAddress
他描述了网络上的一个地址 , 也就是 IP 地址 + 端口号

接下来 , 我们来写一个 UDP版本的回显服务器-客户端 (echo server)

回显服务器 : 客户端发什么 , 服务器返回什么
不涉及任何业务逻辑 , 单纯演示 API 的用法

我们创建两个类 : UdpEchoServer(回显服务器) 和 UdpEchoClient(回显客户端)
先编写服务器代码

2.1.2 服务器代码

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer 
    // 要想创建 UDP 服务器,就需要先打开一个 socket 文件
    // 先不进行初始化
    private DatagramSocket socket = null;

    // 在构造方法中进行初始化
    // 创建实例的同时指定端口号(绑定一个端口) -> 把一个进程和一个端口号关联起来
    public UdpEchoServer(int port) throws SocketException 
        socket = new DatagramSocket(port);
    

    // 启动服务器
    public void start() throws IOException 
        System.out.println("服务器已经启动");
        // 服务器是 7*24 运行的
        while (true) 
            // 1. 读取客户端发来的请求
            // 尝试读取,并不是调用了就一定能读到
            // 如果客户端没有发来请求,就会阻塞等待
            // 直到真的有客户端的请求过来了,receive才会返回
            // receive 属于输出型参数
            // 我们需要先构造参数,然后将数据写入到输出型参数中
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);

            // 2. 对请求进行解析(把 DatagramPacket 转换成 String)
            // requestPacket 内部就是字节数组,用 getData 将内部的字节数组取出来
            // 再设置从哪到哪取出
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 3. 根据请求处理响应
            // 虽然咱们这里是个回显服务器,但是还是可以单独搞个方法处理响应
            String response = process(request);

            // 4. 把响应构造成 DatagramPacket 对象
            // 第一个参数:把 String 转换成字节数组
            // 第二个参数:返回的字节数组的长度
            // 第三个参数:构造响应对象->谁给咱们发来的请求,就把响应发给谁
            DatagramPacket responsePacket = new DatagramPacket(
                    response.getBytes(),
                    response.getBytes().length,
                    requestPacket.getSocketAddress()
            );

            // 5. 把 DatagramPacket 返回
            socket.send(responsePacket);

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

    // 根据请求处理响应
    // 回显服务器:不用处理任何逻辑
    public String process(String request) 
        return request;
    

    // 启动服务器
    public static void main(String[] args) throws IOException 
        // 端口号可以在一定范围内(0~65535)随便写
        // 一般来说,1024以下的端口是系统留着自己用的,我们就不跟他抢了
        UdpEchoServer server = new UdpEchoServer(8000);
        server.start();
    


其中 , 服务器的代码中有许多问题 , 我们也在这张图片里面给大家讲解了

再去编写客户端的代码

2.1.3 客户端代码

package network;

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

public class UdpEchoClient 
    // 还是一样,先设置成 null
    private DatagramSocket socket = null;

    // 客户端的端口号一般都是操作系统自动分配的
    public UdpEchoClient() throws SocketException 
        socket = new DatagramSocket();
    

    public void start() throws IOException 
        // 前置工作:让用户往控制台中输入请求内容
        Scanner sc = new Scanner(System.in);

        while (true) 
            // 1. 让客户端从控制台读取一个请求数据
            System.out.print("> ");
            String request = sc.next();

            // 2. 把这个字符串请求发送给服务器
            // 就需要构造 DatagramPacket
            // 构造的 DatagramPacket 既要包含要传输的数据,还要指定要把数据传输到哪里
            DatagramPacket requestPacket = new DatagramPacket(
                    request.getBytes(),
                    request.getBytes().length,
                    InetAddress.getByName("127.0.0.1")
                    , 8000
            );

            // 3. 把数据报发给服务器
            socket.send(requestPacket);

            // 4. 从服务器读取响应数据
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);

            // 5. 把响应的数据获取出来,转成字符串
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());

            // 6. 打印报文
            System.out.printf("req:%s,resp:%s\\n", request, response);

        
    

    public static void main(String[] args) throws IOException 
        UdpEchoClient client = new UdpEchoClient();
        client.start();
    



我把客户端的代码中常见问题也展示在下方了

我们来梳理一下客户端和服务器的工作流程

这个事情 , 就相当于客户端想干一件事 , 但是不想亲力亲为 , 就要安排给别人做
上述流程 , 不仅仅是回显服务器客户端如此 , 大部分的客户端服务器都是如此 . 这是一套基本套路

我们运行一下

另外 , 我们的服务器是可以给多个客户端提供服务的
我们可以再运行一个客户端 , 不过需要设置



把这个选项勾选上 , 就可以启动多个客户端了

一个服务器的灵魂所在 , 就是 process 方法
一个服务器要完成的工作 , 都是通过 “根据请求计算相应” 来体现的 .
不管是啥样的服务器 , 读取请求并解析 , 构造响应并返回 , 这两个步骤 , 大同小异
唯有 “根据请求计算相应” 是千变万化的 , 是非常复杂的 , 可能一次处理请求就要经历 几w 几十w行 的代码来完成
如果我现在不是想写一个回显服务器了 , 而是一个带有业务逻辑的服务器 , 基于上述框架 , 稍作修改即可

举个栗子 : 我们新建一个 UdpDictServer 类(字典服务器 / 翻译服务器)
我们希望实现一个 英译汉 的效果
请求是一个英文单词 , 响应是他对应的翻译
UdpDictServer 只需要继承我们之前写过的 UdpEchoServer 即可

package network;

// 翻译服务器
// 我们希望实现一个 英译汉 的效果
// 请求是一个英文单词 , 响应是他对应的翻译

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

// 我们之前已经实现过最基础的翻译服务器
// 只需要继承这个最简单的服务器,然后在这基础上稍加改动即可
public class UdpDictServer extends UdpEchoServer 
    private HashMap<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException 
        super(port);

        // 我们在构造方法中构造数据
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("ox", "小牛");
    

    // 和父类的 UdpEchoServer 相比
    // 只需要修改 process 方法
    @Override
    public String process(String request) 
        // getOrDefault:哈希表中,有就返回.没有,就返回"我也不会"
        return dict.getOrDefault(request, "这个词我也不会");
    

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

客户端还用我们刚才写好的即可
运行一下

2.1.4 通过代码分析 UDP 的特点

UDP 是无连接的

UDP 是不可靠传输 , 这个在我们的代码中是看不出来的 , 是操作系统就这样实现的
UDP 是面向数据报的

UDP 是全双工的

2.1.5 小结

总结一下 : UDP 版本的 Socket API 有两个核心类
DatagramSocket : 表式内核里面的 socket 文件

  • send
  • receive
  • close

DatagramPacket : 发送接收数据的基本单位
构造方法 :

  1. 只包含缓冲区 , 用于接收数据的时候 , 构造了一个空的数据报.
  2. 包含缓冲区和一个 InetAddress , 用于发送数据的时候 , 指定数据包含啥以及发到哪里去.
  3. 包含缓冲区和一个 InetAddress + int , 用于发送数据的时候 , 指定数据包含啥以及发到哪里去.

但是我们刚才写的代码 , 并没有去 close 啊
文件打开之后 , 用完了就要及时关闭啊 , 那上述代码中为啥没 close ?
我们的服务器一般都是 7*24 小时运行的 , 上述代码中的 socket 对象 , 生命周期应该是伴随整个进程的
因此 , 进程结束之前 , 提前关闭 socket 对象是不合适的
而且如果进程已经结束 , 对应的 PCB 也就没了 , PCB 中的文件描述符表也就没了 . 此时也就相当于是关闭了

2.1.6 当我们发现某个端口 , 被其他进程占用了 , 单只咱们服务器启动不起来 , 怎么办 ?

使用 netstat 命令来找到是哪个进程占用的
netstat -ano | findstr "8000"

通过这条指令 , 就可以找到对应进程的 PID 了
我们就可以去任务管理器中找到对应线程然后关闭它

2.2 TCP 的 Socket API

2.2.1 引子

在 TCP 的 Socket API 中 , 主要涉及两个主要的类 :
ServerSocket API : 服务器端使用的 Socket
构造方法 : ServerSocket(int port) , 创建一个服务端流套接字Socket,并绑定到指定端口
核心方法 :
accept() : accept没有参数 , 返回值是一个 Socket 对象 , 功能是等待有客户端和服务器建立上连接.
accept 则会把这个连接获取到进程中 , 进一步的通过返回值的 Socket 对象来和客户端进行交互

Socket : 服务器和客户端都会使用的 Socket
通过 Socket 对象,就可以进行发送/接收数据了.
核心方法 :
InputStream getInputStream()
OutputStream getOutputStream()
通过这两个方法 , 传输数据不再通过 Socket 对象 , 而是 Socket 内部包含了输入流对象和输出流对象
借助输入流对象来去读数据 , 也就是接收数据
借助输出流对象来去写数据 , 也就是发送数据

举个栗子 : 比如我们去万达吃饭 , 一般每个店铺都会让一个人专门在门口拉客
一位小哥哥把我们叫住 , 问我们想不想吃火锅 , 我们就跟他进去了
进去店铺之后 , 小哥哥就把我们交给一位小姐姐 , 接下来就是这个美女来服务我们
这种店铺是有明确分工的 , 小哥哥是外场拉客的 , 小姐姐是内场提供服务的
外场拉客就相当于 ServerSocket
内场服务就相当于 Socket
当外场小哥哥拉到了客人 (有连接过来了) , 就通过 accept 把连接交给了 Socket , 后续就通过 Socket 对象和客户端进行沟通

2.2.2 服务器的代码

package network;

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) 
            // accept 干的活是拉客
            // 对于操作系统来说,建立一个 TCP 连接,是内核操作
            // accept 要干的就是等连接建立好了,把这个连接给拿到应用程序中
            // 如果当前连接还未建立,accpet就会阻塞等待
            // accept 相当于有人给你打电话了,你按下接听键.如果没人给你打电话,你就会阻塞等待直到有人给你打电话
            Socket clientSocket = serverSocket.accept();

            processConnect(clientSocket);
        
    

    // 通过这个方法,给当前连接上的客户端提供服务
    // 一个连接过来了,服务方式可能有两种
    // 1. 一个连接只进行一次数据交互(一个请求+一个响应)  短连接
    // 2. 一个连接进行多次数据交互(N个请求+N个响应)     长连接
    // 此处是长连接的版本
    public void processConnect(Socket clientSocket) throws IOException 
        // 在最开始获取一下 IP地址和端口号
        System.out.printf("[%s,%d] 建立连接\\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        /*
        // 接收数据
        InputStream inputStream = clientSocket.getInputStream();
        // 发送数据
        OutputStream outputStream = clientSocket.getOutputStream();
        */

        // 使用字节流是不方便的
        // 我们就可以用 Scanner PrintWriter 来对他们进行封装
        try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) 
            // 接收数据
            Scanner scanner = new Scanner(inputStream);
            // 发送数据
            PrintWriter printWriter = new PrintWriter(outputStream);
            // 长连接的写法 -> 循环
            while (true) 
                // 判断还有没有数据
                // 读完了,后面没有数据,就相当于读到了 EOF
                // 这个操作就代表客户端断开连接,此时 hasNext 返回 false
                if (!scanner.hasNext()) 
                    // 连接断开
                    // 打印一下哪个 IP+端口 连接断开了
                    System.out.printf("[%s,%d] 断开连接\\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                
                // 1. 读取请求并解析
                String request = scanner.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回客户端
                printWriter.write(response);
                // 刷新一下缓冲区,避免数据真的没有发出去
                printWriter.flush();
                // 每次发送完毕,再来打印一下相关信息
                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 Flutter项目实战之网络抓包 

前言

首先在项目实战中,网络请求与接口的交互是必不可少的操作,但是移动端开发者没有 Web开发者那样完善的抓包工具,所以我们就需要一些抓包软件来替代浏览器的控制台来对APP的网络请求进行抓包分析数据,从而更有利于我们的日常工作。本次笔者将通过Flutter与抓包软件fiddler的双重教程,既可以让 Flutter开发者学习到抓包,又能让其他移动端开发者学习到Fidder工具的使用。

首先认识一下抓包软件 Fiddler。可能早期的开发者都认识这款开发工具,因为这款软件不如 charles那样跨平台,再加上很多开发者都是用 Mac 开发,所以饱受诟病。但是 Fiddler 现在已经不是以前的 Fiddler了,而且是 Fiddler Everywhere,已经可以在 多个主流系统平台上完美运行了。笔者将内容分为两个部分 Fiddler 跟 Flutter,另外会使用 Mac跟 Windows分别演示效果。

环境

软件或框架名称作用
Fiddler EveryWhere 2.0.0用于抓包
dio 3.1.0Flutter的网络请求框架,相当于Android的 OKHttp,可以配置代理

前期准备

先查看自己的IP地址

Mac OS X

控制台命令: ifconfig

图 1

Windows

控制台命令:ipconfig

图2

Fiddler教程

Fidder 官网 下载成功之后,使用默认选项直接安装即可。

笔者使用的最新版 2.0.2,安装成功之后进入登录页.如图3

图3

如果没有账号的话,可以点击 “New User?SIgn up” 注册。

进入 Fiddler 首页之后

图4

其实Mac跟Windows下的首页都差不多,所以就不放Windows的图了。

先去 “设置”,点击下图 画圈的位置,如图5

图5
弹出设置的对话框,先点击connections选项卡,查看黄色画圈的是监听的端口号“8866”,还有要(红色画圈)检查 “Allow remote computers to connect” 这个要勾中,因为我之前出现过一种情况那就是 明明手机跟电脑连接的同一个Wifi但是无法抓取到 我的应用的网络,后来经过排查发现这个选项没有被勾中导致的。如**图6**

图6

这样就可以配置 HTTPS证书,从而完成抓取HTTPS的请求网络数据,如图7。可能有读者对HTTPS的原理流程不了解的,可以 参考一下笔者的 这篇博客 HTTPS流程详解 希望能对您有所帮助。

图7

这样 初步的配置就完成了。可以抓取浏览器的数据了。

Flutter 配置 抓包

Flutter 1.22.6.stable
dio 3.0.10

 	Dio dio = Dio();
    dio.options.connectTimeout = 1000 * 30;  // 设置最大连接超时时间 30秒
    dio.options.receiveTimeout = 1000 * 30;  // 设置最大接受超时时间 30秒
    dio.options.contentType = Headers.jsonContentType;  // 设置数据的返回格式 json
    dio.options.headers['channel'] = Platform.operatingSystem; // 设置自定义的 请求头 channel,值为系统平台的名称;如果为Android平台,则为 channel: Android
    if (kDebugMode)  // 当前运行模式为 debug调试模式下开启代理,避免上线出现问题,设置代理的服务的URL 为 192.168.1.5:8866,也就是笔者电脑的 IP地址
      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client)  
          client.findProxy = (uri) 
            return 'PROXY 192.168.1.5:8866';
          ;
      ;

通过上面的注释,相信读者应该会很了解,并且能根据自己的实际项目进行配置。

但是有个问题就是如果抓取的请求太多,我们如何做有效的过滤呢?

所以 笔者的技巧就是加上专有的 请求头,也就是上文代码 的 channel这个请求头,既可以标记 请求的客户端平台,又可以对抓包有效过滤,一举两得。可以在图8所示,点击画圈的那个图标。

图8

然后弹出对话框,如图9所示

图9

只捕获中请求头包含有 “channel:iOS”的 HTTP请求,这样就把项目以外的请求全部筛选掉。

有的时候 项目中可能有一些周期性的请求,比如 “埋点”,“定时网络任务”等,这个时候这些请求容易干扰,也需要根据 其他条件来对 请求进行更进一步的过滤。比如我想过滤掉这种 周期性的请求,可以筛选掉URL带有"uploadTrack“的请求

图10

点击 URL 下的 三个小点点的图标,如图11

图11

弹出筛选菜单,如图12所示配置

图12

这样就可以通过匹配URL 过滤或者只保留一些特定的URL请求记录,每个列都有 三点按钮 可以用来过滤一些匹配的信息,读者可自行练习

还可以配置根据接口的请求顺序排序
点击图13 圈出来的 向上箭头,切换接口是按照 从新到旧排序,还是从旧到新排序

图13

注意事项

因为Fiddler EveryWhere是通过设置代理的方式来抓包,如果 Fiddler EveryWhere 没有通过正常操作来关闭 ,在 Windows 10 平台 将会 出现 “断网” 现象,解决方案如下图 14-15所示,将 使用代理服务器 切换成关闭。

图14

图 15

以上是关于Java Web 实战 15 - 计算机网络之网络编程套接字的主要内容,如果未能解决你的问题,请参考以下文章

linux笔记web群集之LVS-DR实战

Flutter 项目实战之网络抓包

Flutter 项目实战之网络抓包

Flutter 项目实战之网络抓包

Flutter 项目实战之网络抓包

Day785.网络通信优化之通信协议:如何优化RPC网络通信 -Java 性能调优实战