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 : 发送接收数据的基本单位
构造方法 :
- 只包含缓冲区 , 用于接收数据的时候 , 构造了一个空的数据报.
- 包含缓冲区和一个 InetAddress , 用于发送数据的时候 , 指定数据包含啥以及发到哪里去.
- 包含缓冲区和一个 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.0 Flutter的网络请求框架,相当于Android的 OKHttp,可以配置代理
前期准备
先查看自己的IP地址
Mac OS X
控制台命令: ifconfig
![](https://image.cha138.com/20221123/5e8f05931f254e759a0038eb3e9a1bbb.jpg)
图 1
Windows
控制台命令:ipconfig
![](https://image.cha138.com/20221123/51046175c035411895001b02cbd6056f.jpg)
图2
Fiddler教程
Fidder 官网 下载成功之后,使用默认选项直接安装即可。
笔者使用的最新版 2.0.2,安装成功之后进入登录页.如图3
![](https://image.cha138.com/20221123/ee5b4c7ab195425a8e9dabd9e5e7e295.jpg)
图3
如果没有账号的话,可以点击 “New User?SIgn up” 注册。
进入 Fiddler 首页之后
![](https://image.cha138.com/20221123/29d31c40767f44129344358949080233.jpg)
图4
其实Mac跟Windows下的首页都差不多,所以就不放Windows的图了。
先去 “设置”,点击下图 画圈的位置,如图5
![](https://image.cha138.com/20221123/66e1fd99fc8d4634a1eb1b098662855f.jpg)
图5
弹出设置的对话框,先点击connections选项卡,查看黄色画圈的是监听的端口号“8866”,还有要(红色画圈)检查 “Allow remote computers to connect” 这个要勾中,因为我之前出现过一种情况那就是 明明手机跟电脑连接的同一个Wifi但是无法抓取到 我的应用的网络,后来经过排查发现这个选项没有被勾中导致的。如**图6**
![](https://image.cha138.com/20221123/276fa1944fac42d899256cebd5e3bef2.jpg)
图6
这样就可以配置 HTTPS证书,从而完成抓取HTTPS的请求网络数据,如图7。可能有读者对HTTPS的原理流程不了解的,可以 参考一下笔者的 这篇博客 HTTPS流程详解 希望能对您有所帮助。
![](https://image.cha138.com/20221123/2a519696e5c44fa5958f5f5e9fe7b72e.jpg)
图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所示,点击画圈的那个图标。
![](https://image.cha138.com/20221123/b9b9bcb76ccf4fd6bb79aa6f2f6e9b71.jpg)
图8
然后弹出对话框,如图9所示
![](https://image.cha138.com/20221123/121327d299a043e0af2e35255260ce6d.jpg)
图9
只捕获中请求头包含有 “channel:iOS”的 HTTP请求,这样就把项目以外的请求全部筛选掉。
有的时候 项目中可能有一些周期性的请求,比如 “埋点”,“定时网络任务”等,这个时候这些请求容易干扰,也需要根据 其他条件来对 请求进行更进一步的过滤。比如我想过滤掉这种 周期性的请求,可以筛选掉URL带有"uploadTrack“的请求
![](https://image.cha138.com/20221123/4603ec901d6d43d4bd0eb931eeb4fb0c.jpg)
图10
点击 URL 下的 三个小点点的图标,如图11
![](https://image.cha138.com/20221123/f6f30b59fc5f421bb004e4577ff97291.jpg)
图11
弹出筛选菜单,如图12所示配置
![](https://image.cha138.com/20221123/d36c4e9fb03f48ce807ec79aeb49baaf.jpg)
图12
这样就可以通过匹配URL 过滤或者只保留一些特定的URL请求记录,每个列都有 三点按钮 可以用来过滤一些匹配的信息,读者可自行练习
还可以配置根据接口的请求顺序排序
点击图13 圈出来的 向上箭头,切换接口是按照 从新到旧排序,还是从旧到新排序
![](https://image.cha138.com/20221123/5e68575f55b446da9c747ed73a2d425a.jpg)
图13
注意事项
因为Fiddler EveryWhere是通过设置代理的方式来抓包,如果 Fiddler EveryWhere 没有通过正常操作来关闭 ,在 Windows 10 平台 将会 出现 “断网” 现象,解决方案如下图 14-15所示,将 使用代理服务器 切换成关闭。
![](https://image.cha138.com/20221123/2e8849f3a2cf47239805f23cf1712c59.jpg)
图14
![](https://image.cha138.com/20221123/b9b923923eb04b2b9d56da0afdfc6f32.jpg)
图 15
以上是关于Java Web 实战 15 - 计算机网络之网络编程套接字的主要内容,如果未能解决你的问题,请参考以下文章