Java EE:6.网络编程套接字(第一弹)

目录

1.网络编程基础(课件内容)

[1.1 为什么需要网络编程?------丰富的网络资源](#1.1 为什么需要网络编程?——丰富的网络资源)

[1.2 什么是网络编程](#1.2 什么是网络编程)

[1.3 网络编程中的基本概念](#1.3 网络编程中的基本概念)

发送端和接收端

请求和响应

客户端和服务端

常见的客户端服务端模型

2.Socket

(1)有连接vs无连接

答疑

(2)可靠传输vs不可靠传输

(3)面向字节流vs面向数据报

(4)全双工vs半双工

课件内容

[2.1 概念](#2.1 概念)

[2.2 分类](#2.2 分类)

流套接字:使用传输层TCP协议

数据包套接字:使用传输层UDP协议

原始套接字

Java数据报套接字通信模型

Java流套接字通信模型

Socket编程注意事项

3.UDP数据报套接字编程

[3.1 API 介绍](#3.1 API 介绍)

DatagramSocket(核心)

DatagramPacket(核心)

InetSocketAddress

[3.2 UDP版本服务器代码示例](#3.2 UDP版本服务器代码示例)

[UDP Echo Server(服务器)](#UDP Echo Server(服务器))

答疑

[1.创建 Socket 对象并指定端口号](#1.创建 Socket 对象并指定端口号)

[2.在 start 方法里创建一个主循环](#2.在 start 方法里创建一个主循环)

3.读取请求并解析~~

[a)构造 DatagramPacket 对象](#a)构造 DatagramPacket 对象)

[b)调用 receive](#b)调用 receive)

[c)把 UDP 数据包载荷取出来,构造成一个 String](#c)把 UDP 数据包载荷取出来,构造成一个 String)

4.根据请求计算响应~~

5.把响应返回给客户端~~

[1)response.getBytes() :拿到字符串中的字节数组~~](#1)response.getBytes() :拿到字符串中的字节数组~~)

2)response.getBytes().length:拿到字节数组的长度,而不是使用字符串长度(单位:字符)

[3)requestPacket.getSocketAddress():拿到客户端的 IP 和 端口号~~](#3)requestPacket.getSocketAddress():拿到客户端的 IP 和 端口号~~)

[4)new DatagramPacket 是要干啥??](#4)new DatagramPacket 是要干啥??)

5)把构造好的数据报送出去

6.记录这次的客户端/服务器的交互过程~~

答疑

完整服务器代码

[UDP Echo Client(客户端)](#UDP Echo Client(客户端))

完整客户端代码

回显服务器与客户端的交互

答疑

云服务器

答疑

阶段性小结

[UDP Dict Server(翻译服务器)](#UDP Dict Server(翻译服务器))

完整代码

交互演示

答疑


1.网络编程基础(课件内容)

1.1 为什么需要网络编程?------丰富的网络资源

用户在浏览器中,打开在线视频网站,如B站看视频,实质是通过网络,获取到网络上的一个视频资源

与本地打开视频文件类似,只是视频文件这个资源的来源是网络

相比本地资源来说,网络提供了更为丰富的网络资源:

所谓的网络资源,其实就是在网络中可以获取的各种数据资源

而所有的网络资源,都是通过网络编程来进行数据传输的

1.2 什么是网络编程

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

当然,我们只要满足进程不同就行,所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程

特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程

但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:

进程A:编程来获取网络资源

进程B:编程来提供网络资源

1.3 网络编程中的基本概念

发送端和接收端

在一次网络数据传输时:

发送端:数据的发送方进程,称为发送端,发送端主机即网络通信中的源主机

接收端:数据的接收方进程,称为接收端,接收端主机即网络通信中的目的主机

收发端:发送端和接收端两端,也简称为收发端

注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流后的概念

请求和响应

一般来说,获取一个网络资源,涉及到两次网络数据传输:

第一次:请求数据的发送

第二次:响应数据的发送

好比在快餐店点一份炒饭:

先要发起请求:点一份炒饭,再有快餐店提供的对应响应:提供一份炒饭

客户端和服务端

服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务

客户端:获取服务的一方进程,称为客户端

对于服务来说,一般是提供:

客户端获取服务资源

客户端保存资源在服务端

好比在银行办事:

银行提供存款服务:用户(客户端)保存资源(现金)在银行(服务端)

银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管现金)

常见的客户端服务端模型

最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:

1.客户端先发送请求到服务端

2.服务端根据请求数据,执行相应的业务处理

3.服务端返回响应:发送业务处理结果

4.客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

2.Socket

通过前面的知识我们知道,程序员工作中主要跟应用层打交道,也就是我们编写的程序,而我们编写的程序是需要把这样的数据交给传输层的,而传输层怎么去完成这个数据传递的过程?

使用操作系统提供的一组 API=> Socket API(传输层给应用层提供)

Socket 本身叫做 插槽,其实是指主板上的一些接口~~

这个Socket API 是我们需要密切关注的,而其他的协议我们相对来说并不关心

答疑:那为啥面试要考??

其实面试也不是都考,主要考应用层和传输层

网络层、数据链路层、物理层,考的相对比较少的~~

传输层涉及到两个核心协议:TCP 和 UDP

这两种协议差别非常大,编写代码的时候,也是不同的风格~~

为了都能够提供,Socket API 提供了两套API~~

TCP:有连接,可靠传输,面向字节流,全双工

UDP:无连接,不可靠传输,面向数据报,全双工

(1)有连接vs无连接

要进行网络通信,物理上的连接(网线啥的)肯定是要有的

而此处我们说的连接,是一个抽象的概念,虚拟的/逻辑上的连接

打个比方,你和一个妹子结婚~~

需要干两件事:领证+办酒席

而结婚证就是属于证明婚姻关系最核心的材料~~

你和妹子领证,就是"建立连接"的过程~~

结婚证,一式两份,双方一人拿一个~~

你拿着结婚证,上面就写着,你的媳妇儿是"白富美"

妹子拿着结婚证,上面就写着,她的老公是"高富帅"~~

这种就是虚拟的,抽象的连接,彼此之间保存对方的信息

闲聊:办个假证~~

这属于伪造国家公文公章罪~~可不敢这么做~~

网上,找打印店,PS 一个公章......

这都很刑了~~都会进去的!!都不是违法行为,妥妥的犯罪行为~~

对于 TCP 来说,TCP 协议中,就保存了对端的信息

A 和 B 通信,A 和 B 先建立连接

让 A 保存 B 的信息,B 保存 A 的信息(彼此之间知道,谁是和他建立连接的那个)

对于 UDP 来说,UDP 协议本身,不保存对方的信息~~就是无连接

当然,你可以在你自己代码中写变量,来保存对方的信息,但这不是 UDP 的行为,这是你自己的行为

答疑

Q1:不保存信息不会敲错门吗??

UDP 中取决于你代码咋写,敲错门也可能存在的~~

Q2:连接的话不就是耦合了吗??

确实如此~~但是耦合也不是完全要规避的

要实现某个效果,就需要付出一些代价

TCP 为了支持后续有用的特性,引入连接,付出"耦合"的代价~~

但是总体还是利大于弊的~~

Q3:对端是别的应用程序吗??

网络通信中,通信的两端的程序大概率是不同的程序

QQ音乐的客户端和QQ音乐的服务器,肯定不是同一个程序~~

当然,不能拿QQ音乐的客户端和 cctalk 的服务器进行通信

因为协议上是不兼容的,QQ音乐客户端有一套自己的应用层协议,cctalk 也是有一套~~

还是之前协议的知识,如果协议兼容,就可以通信~~

(2)可靠传输vs不可靠传输

网络上,数据是非常出现丢失的情况(丢包)~~

光信号/电信号,都可能受到外界的干扰~~

本来是传输0101,其中有些 bit 位就被修改了~~

这样乱了的数据就会被识别出来,把这样的数据给丢弃掉~~

闲聊:像太阳耀斑爆发就会对卫星产生影响~~

丢包还有一种情况,我们知道网络世界是通过 路由器/交换机 来构建的

路由器/交换机,类似于"十字路口",是否会"堵车"??

某个时间点,实际需要转发的数据超过了设备能转发的上限~~

闲聊:像前几年上学的时候,宿舍宽带也就 1Mbps,不像现在动不动就 1000Mbps,当时四个人共用一个宽带,一旦有某个兄弟在下片儿,此时其他人就会很卡~~

我们不能指望,一个数据包发送之后,100%到达对方~~

可靠传输的意思,不是保证数据包100%到达,而是尽可能的提高传输成功的概率

并且如果出现丢包了,能够感知到~~

不可靠传输,就只是把数据发了,就不管了~~

闲聊:直观感觉,可靠传输更好??

可靠传输也是要付出代价的~~比如效率就相对会降低~~

(3)面向字节流vs面向数据报

面向字节流:

读写数据的时候,是以一个字节为单位

支持任意长度=>但存在"粘包问题"

面向数据报:读写数据的时候,是以一个数据报位单位(不是字符),一次必须读写一个 UDP 数据报,不能是半个~~

不存在粘包=>但是有长度限制~~

后面 TCP 详细解释~~

(4)全双工vs半双工

全双工:一个通信链路,支持 双向通信(能读,也能写)

半双工:一个通信链路,只支持单向通信(要么读,要么写)

答疑:不读怎么写??

比如咱们直播的时候,咱的网卡就在持续不断的写数据(发送数据),这个过程就不涉及读数据

闲聊:俺有个朋友,是个兽医~~

有一天给马喂药,跟徒弟说,把这个管放马嘴里,吹气儿就喂好了~~然后就出去了

等回来的时候发现,大徒弟躺地上一动不动,口吐白沫~~

赶紧扶起来,大徒弟你咋啦???

一问原来是,放完管,马先吹气了~~

这就属于半双工~~

类似马路上的单行车道,而全双工,就类似马路上的双车道/四车道~~

课件内容

2.1 概念

Socket套接字,是由系统提供用于网络通信的技术,是基于 TCP / IP 协议的网络通信的基本操作单元

基于Scoket套接字的网络程序开发就是网络编程

2.2 分类

Socket套接字主要针对传输层协议划分为如下三类:

流套接字:使用传输层TCP协议

TCP,即Transmission Control Protocol(传输控制协议),传输层协议

TCP的特点:有连接,可靠传输,面向字节流,有接收缓冲区,也有发送缓冲区(全双工),大小不限

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收

数据包套接字:使用传输层UDP协议

UDP,即User Datagram Protocol(用户数据报协议),传输层协议

UDP的特点:无连接,不可靠传输,面向数据报,有接收缓冲区,也有发送缓冲区(全双工),大小受限(一次最多传输64k)

对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节

原始套接字

原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据

我们不学习原始套接字,简单了解即可

Java数据报套接字通信模型

对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报

Java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 作为发送或接收的UDP数据报,对于一次发送及接收UDP数据报的流程如下:

以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据,也就是只有请求,没有响应,对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:

Java流套接字通信模型

Socket编程注意事项

1.客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同主机

2.注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程

3.Sokcet编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议,也需要考虑,这块我们在后续来说明如何设计应用层协议

4.关于端口被占用的问题

5.如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用,对于Java进程来说,端口被占用的常见报错信息如下:

此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用,以下为通过端口号查进程的方式:

在 cmd 输入 netstat -ano | findstr 端口号,则可以显示对应进程的PID,如以下命令显示了8888进程的PID

在任务管理器中,通过PID查找进程

解决端口被占用的问题:

如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B

如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口
我们先写一些代码感受一下,后续再拿出来详细讲解,尤其是 TCP 协议,这是个非常重要的协议,既是面试中重点考的协议,也是工作中经常涉及到的协议

下面就要用 Socket API 进行网络编程了,本身也是操作系统的功能~~

操作系统提供的内容,一般都是需要封装之后才能使用的,咱们的 JDK 也对其进行封装了,下面就拿Java已经封装好了的 API 进行操作即可

3.UDP数据报套接字编程

3.1 API 介绍

DatagramSocket(核心)

我们之前讲过,计算机中的"文件"通常是一个"广义的概念"

文件还能代指一些硬件设备(操作系统管理硬件设备,也是抽象成文件,统一管理的)

网卡在系统中就被抽象成了一个 Socket 文件(特指网卡设备)(此处说的就是电脑的网卡👇)

操作网卡的时候,流程和操作普通文件差不多

都是 打开→读写→关闭

打开的时候有也会在文件描述符表中分配一个表项

其实我们直接操作网卡,直接操作不好操作

于是就把操作网卡转换成操作 Socket 文件,Socket 文件就相当于"网卡的遥控器"

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报

DatagramSocket 构造方法:功能→打开文件

|--------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务器) |

其中 port 就是端口号,创建 Socket 的时候,就会关联上一个 端口号

我们使用端口号区分主机上不同的应用程序~~

我们可以使用随机端口(无参构造方法),也可以指定一个固定的端口,必然是有端口,正如学习MySQL的时候端口号是3306,五元组中也要求要有 源端口和目的端口~~

DatagramSocket 方法:功能→读写

|--------------------------------|---------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |

receive(接收)→读,send(发送)→写,其中参数 DatagramPacked 就表示一个完整的 UDP 数据报

我们发现,其实跟之前学的文件都是类似的,先打开再读写再关闭~~

注意:

void receive(DatagramPacket p) 这里也是把参数作为"输出的结果",属于输出型参数,与之前讲 IO 流中的 read 是一样的效果~~

使用这个方法之前,需要先构造 空的(不是null),把对象传递给 receive 里面,receive 就会把数据从网卡读出来,填充到参数中

DatagramPacket(核心)

DatagramPacket 是UDP Socket 发送和接收的数据报

DatagramPacket 构造方法:

|--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte\[\] buf,int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
| DatagramPacket(byte\[\] buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |

UDP 数据报的载荷数据,就可以通过构造方法来指定~~

可以指定一个字节数组传进去,也可以指定一个字节数组的同时,再指定一个地址传进去

DatagramPacket 方法:

|--------------------------|--------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
| int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
| byte\[\] getData() | 获取数据报中的数据 |

构造UDP发送的数据报时,需要传入 SocketAddress,该对象可以使用 InetSocketAddress 来创建

InetSocketAddress

InetSocketAddress(ScoketAddress 的子类)构造方法:

|----------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号 |

3.2 UDP版本服务器代码示例

UDP Echo Server(服务器)

java 复制代码
package network;
/**Echo 回声~~
//我们在这里通过称为回显服务器~~
//客户端给服务器发一个数据(请求)
//服务器返回一个数据(响应)
//回显服务器:请求是啥,响应就是啥~~
//真实的额服务器,请求和响应是不一定的~~
//但是当前阶段,先不考虑那些,先把API用起来,写个回显~~
 */
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;//net:网络
import java.net.SocketException;

public class UdpEchoServer {
    private DatagramSocket socket=null;//此处的private可省略
    //提供一个构造方法
    public UdpEchoServer(int port) throws SocketException {
        //SocketException:网络编程中常见的异常
        //指定了一个固定端口号,让服务器来使用
        socket=new DatagramSocket(port);
    }
    public void start() throws IOException {
        //启动服务器
        System.out.println("服务器启动");
        //对于服务器来说,客户端啥时候发请求,发多少个请求,我们无法预测
        //因此服务器中通常都需要有一个死循环,持续不断的尝试读取客户端的请求数据~~
        //即 7×24 小时运行~~
        while(true){
            //循环一次,就相当于处理一次请求
            //处理请求的过程,典型的服务器都是分成三个步骤的
            //1.读取请求并解析
            //  DatagramPacket 表示一个 UDP 数据报,此处传入的字节数组,就是保存 UDP 的载荷部分
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);//往上抛IOException异常
            //  把读取到的二进制数据,转成字符串,只是构造有效的部分:getData()拿到字节数组,getLength()拿到有效长度
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());

            //2.根据请求,计算响应(服务器最关键的逻辑)
            //  但是此处我们写的是回显服务器,这个环节相当于省略了,请求发啥就返回啥
            String response=process(request);

            //3.把响应返回给客户端
            //  根据 response 构造 DatagramPacket,发送给客户端
            //  此处不能使用 response.length(), 因为 response.length() 返回的是String中字符的个数
            //  而 response.getBytes().length 返回的是String中字节的个数
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length);
            //  此处还不能直接发送,因为 UDP 协议自身没有保存对方的信息(不知道发给谁)
            //  需要指定 目的IP 和 目的端口
            socket.send(responsePacket);
        }
    }
    //后续如果要写别的服务器,只修改这个地方就好了
    private String process(String request) {
        return request;
    }
}

写到这里,需要目的IP 和 目的端口才能进行发送,我们思考,目的IP 和 目的端口 从哪拿到呢??

我们要写的是一个服务器,服务器要做的是返回一个响应的数据,什么是响应??给客户端返回的数据叫做响应,所以响应发给谁,意味着我们的客户端是谁,客户端是谁??看 requestPacket 这个数据报是谁发给你的,接下来就返回给谁

我们收到请求的 源IP,源端口,就是返回响应的 目的IP 和 目的端口~~

如何知道刚才的数据从哪来的呢?就在 requestPacket 里,这里不仅包含了载荷,也包含了五元组的其他信息,所以我们可以在构造 responsePacket 这个对象的时候再加上一个参数👇

java 复制代码
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());

用 requestPacket.getSocketAddress() 来指定地址,getSocketAddress() 返回的对象就包含了 IP 和 端口号

答疑

Q1:载荷部分有 IP 和端口号??

是在报头中的!!

UDP 报头里,包含了 源端口/目的端口

IP 报头里,包含了 源IP/目的IP

而 DatagramPacket requestPacket 虽然说是一个 UDP 数据报,但是这些信息里面也是有的,可以通过它来获取这些 IP 和 端口

Q2:请求怎么在服务端里创建了??

这个就是我们上面说过的 输出型参数,我们创建的是一个空白的对象,然后由 receive 往里填充,里面的数据是客户端发来的~~


再补一个日志,代码基本就完成了👇

java 复制代码
package network;
/**Echo 回声~~
//我们在这里通过称为回显服务器~~
//客户端给服务器发一个数据(请求)
//服务器返回一个数据(响应)
//回显服务器:请求是啥,响应就是啥~~
//真实的额服务器,请求和响应是不一定的~~
//但是当前阶段,先不考虑那些,先把API用起来,写个回显~~
 */
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;//net:网络
import java.net.SocketException;

public class UdpEchoServer {
    private DatagramSocket socket=null;//此处的private可省略
    //提供一个构造方法
    public UdpEchoServer(int port) throws SocketException {
        //SocketException:网络编程中常见的异常
        //指定了一个固定端口号,让服务器来使用
        socket=new DatagramSocket(port);
    }
    public void start() throws IOException {
        //启动服务器
        System.out.println("服务器启动");
        //对于服务器来说,客户端啥时候发请求,发多少个请求,我们无法预测
        //因此服务器中通常都需要有一个死循环,持续不断的尝试读取客户端的请求数据~~
        //即 7×24 小时运行~~
        while(true){
            //循环一次,就相当于处理一次请求
            //处理请求的过程,典型的服务器都是分成三个步骤的
            //1.读取请求并解析
            //  DatagramPacket 表示一个 UDP 数据报,此处传入的字节数组,就是保存 UDP 的载荷部分
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);//往上抛IOException异常
            //  把读取到的二进制数据,转成字符串,只是构造有效的部分:getData()拿到字节数组,getLength()拿到有效长度
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());

            //2.根据请求,计算响应(服务器最关键的逻辑)
            //  但是此处我们写的是回显服务器,这个环节相当于省略了,请求发啥就返回啥
            String response=process(request);

            //3.把响应返回给客户端
            //  根据 response 构造 DatagramPacket,发送给客户端
            //  此处不能使用 response.length(), 因为 response.length() 返回的是String中字符的个数
            //  而 response.getBytes().length 返回的是String中字节的个数
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            //  此处还不能直接发送,因为 UDP 协议自身没有保存对方的信息(不知道发给谁)
            //  需要指定 目的IP 和 目的端口
            socket.send(responsePacket);

            //4.打印一个日志
            System.out.printf("[%s:%d] req: %s, resp: %s\n",
                    requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }
    //后续如果要写别的服务器,只修改这个地方就好了
    private String process(String request) {
        return request;
    }
}

虽然代码只有简单的五十多行,但是我们会发现理解起来比较困难~~

理想的学习模式,每次只介绍一个新的知识~~

但是网络这里,新的类,新的方法,新的概念,新的流程,一股脑就涌过来了~~

想写一个最简单的网络程序~~

很遗憾,当前的代码,已经是最简单的了,无法再精简了~~


下面重新给大家捋一遍流程👇

1.创建 Socket 对象并指定端口号
java 复制代码
public class UdpEchoServer {
    private DatagramSocket socket=null;//此处的private可省略
    //提供一个构造方法
    public UdpEchoServer(int port) throws SocketException {
        //SocketException:网络编程中常见的异常
        //指定了一个固定端口号,让服务器来使用
        socket=new DatagramSocket(port);
    }

因为 Socket 对象代表网卡文件,读这个文件等于从网卡收数据,写这个文件等于让网卡发数据~~

2.在 start 方法里创建一个主循环
java 复制代码
    public void start() throws IOException {
        //启动服务器
        System.out.println("服务器启动");
        //对于服务器来说,客户端啥时候发请求,发多少个请求,我们无法预测
        //因此服务器中通常都需要有一个死循环,持续不断的尝试读取客户端的请求数据~~
        //即 7×24 小时运行~~
        while(true){
            //循环一次,就相当于处理一次请求
            //处理请求的过程,典型的服务器都是分成三个步骤的

在主循环中,做三件事~~

java 复制代码
//1.读取请求并解析

//2.根据请求,计算响应(服务器最关键的逻辑)
//  但是此处我们写的是回显服务器,这个环节相当于省略了,请求发啥就返回啥

//3.把响应返回给客户端

这三件事儿是一个服务器通常的流程~~

3.读取请求并解析~~
java 复制代码
//1.读取请求并解析
//  DatagramPacket 表示一个 UDP 数据报,此处传入的字节数组,就是保存 UDP 的载荷部分
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);//往上抛IOException异常
//  把读取到的二进制数据,转成字符串,只是构造有效的部分:getData()拿到字节数组,getLength()拿到有效长度
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
a)构造 DatagramPacket 对象

DatagramPacket 就代表 UDP 数据包

报头+载荷(new 字节数组保存,因为 String 底层是通过字节数组保存数据的👇)

闲聊:如果这里把字节数组隐藏在 DatagramPacket 里面能更好点~~

b)调用 receive

理解输出型参数~~

c)把 UDP 数据包载荷取出来,构造成一个 String

1)通过 requestPacket.getData() 拿到 DatagramPacket 中的字节数组~~

2)requestPacket.getLength() 拿到有效数据的长度

3)根据字节数组,构造出一个 String :new String()

4.根据请求计算响应~~
java 复制代码
//2.根据请求,计算响应(服务器最关键的逻辑)
//  但是此处我们写的是回显服务器,这个环节相当于省略了,请求发啥就返回啥
String response=process(request);

这里不难理解,包装一下就行了

java 复制代码
//后续如果要写别的服务器,只修改这个地方就好了
private String process(String request) {
    return request;
}
5.把响应返回给客户端~~
java 复制代码
//3.把响应返回给客户端
//  根据 response 构造 DatagramPacket,发送给客户端
//  此处不能使用 response.length(), 因为 response.length() 返回的是String中字符的个数
//  而 response.getBytes().length 返回的是String中字节的个数
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
//  此处还不能直接发送,因为 UDP 协议自身没有保存对方的信息(不知道发给谁)
//  需要指定 目的IP 和 目的端口
socket.send(responsePacket);
1)response.getBytes() :拿到字符串中的字节数组~~
2)response.getBytes().length:拿到字节数组的长度,而不是使用字符串长度(单位:字符)
3)requestPacket.getSocketAddress():拿到客户端的 IP 和 端口号~~

这个方法返回的对象中,同时包含 IP 和 端口~~

4)new DatagramPacket 是要干啥??
java 复制代码
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());

构造响应数据报~~

上面的是"请求数据报",请求和响应的数据报是两个不同的,因此需要再构建一个~~

5)把构造好的数据报送出去
java 复制代码
socket.send(responsePacket);

前提是数据报中包含了 目的IP 和 目的端口 ~~

6.记录这次的客户端/服务器的交互过程~~
java 复制代码
//4.打印一个日志
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);

记录客户端的IP、客户端的端口、请求、响应~~


答疑

Q1:当将 请求数据包 作为输出型参数给receive接收后,请求数据包中就会有源端口、源IP等信息,此时就能通过请求数据包得到源端口和源IP信息构造出响应数据包,哈??

yes!!课代表的总结~~

Q2:那个receive方法的返回型参数还是不太懂~~

拿我们之前文件IO的代码来解释一下👇

相当于 read 的操作会对 buf 这个参数进行修改~~

这个就相当于是 去食堂吃饭,先把一个空的餐盘交给打饭阿姨

人家给你打饭,打完饭把装满饭的餐盘交给你~~

Q3:可不可以说receive根据请求创建响应??

receive 只是在读请求,跟响应没有任何关系~~

这个是不太对的~~

看我们上面的代码👇

java 复制代码
//1.读取请求并解析
//  DatagramPacket 表示一个 UDP 数据报,此处传入的字节数组,就是保存 UDP 的载荷部分
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);//往上抛IOException异常

receive 只是在读请求,后续怎么做,那是后面的事情,跟receive本身是没关系的

后面可能发响应,也可能不发响应,也可能发一个响应,也可能发多个响应

后面发响应跟 receive 没啥关系,只不过当前的回显服务器中,通过 receive 的 源IP 和 源端口 来构造响应数据

Q4:打印出来,参数和返回值同时增加减少?

有客户端,能够和服务器配合通信了,才能做这个事情~~

服务器,一个巴掌拍不响~~

我们在下面编写一个 main 方法,查看运行👇

java 复制代码
    public static void main(String[] args) throws IOException {
        //启动服务器
        UdpEchoServer server=new UdpEchoServer(9090);
        server.start();
    }

运行是能运行,但是没啥效果~~

Q5:socket 不用 close 吗??(这个问题问到心坎上了,没人问,我就要问这个问题了~~)

上述我们写的代码确实没写关闭~~

文件要关闭,需要考虑清楚这个文件对象的生命周期是怎样的~~

此处的 socket 对象,伴随着整个 UDP 服务器,自始至终~~所以不需要关闭~~

如果服务器关闭(进程结束),进程结束时就会自动释放 PCB 的文件描述符表中的所有资源,也不需要手动调用 close 了~~

Q6:为什么第二个 datagrampacket 方法中参数中没有端口号??

你是想问输出型参数的问题吧?还是之前的话,在下面的代码中👇

java 复制代码
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);//往上抛IOException异常

一开始我们构造一个空餐盘,然后用 receive 往里填数据,这个数据就包括了端口号,所以这里没有显式写出来端口号,而后面的 datagrampacket 这个代码👇

java 复制代码
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
//  此处还不能直接发送,因为 UDP 协议自身没有保存对方的信息(不知道发给谁)
//  需要指定 目的IP 和 目的端口
socket.send(responsePacket);

就必须要指定端口号,否则不知道发给谁,发不出去

Q7:是不是 getsocketaddress 包括 IP 和端口号??

你说的是这段代码👇

java 复制代码
//4.打印一个日志
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);

DatagramPacket 有三个方法:

1)getAddress:只拿到 IP

2)getPort:只拿到端口号

3)getSocketAddress:同时拿到 IP 和 端口号(IP 和 端口号 通过一个 InetAddress 对象表示)

IP 和 端口号 是都要传的,要么一起传,要么分开传

我们可以查看一下构造方法👇

Q8:第一步是直接把二进制转成字符串吗?不是要解析吗?

后续客户端给服务器发的数据,就是发字符串

(回显echo这里,客户端发一个 hello,服务器返回 hello)

本身收到的 DatagramPacket 的二进制数据就是从 String 转来的,现在只是还原回去~~

这里的解析就是单纯的二进制转字符串的过程

Q9:二进制在代码层面能解析吗??

当然可以,咱们当前课堂上没有涉及到~~

后面有一个项目,广泛使用到二进制的解析(模拟实现消息队列的项目)

Q10:客户端要搞图形化界面吗??

Java 除非是 Android ,否则不搞图形化开发

虽然Java有这样的功能,比如 Swing、JavaFX

但实际开发中做桌面的程序,不会轮到Java登场的,有更好的选择~~

Q11:JavaFX 可太折磨人了,不好用~~

JavaFX 这一套 API 设计的还好~~

不是说 JavaFX 折磨人,而是 图形化界面开发,这件事本身就很折磨人~~

功能可能不难,但有非常多的细节需要扣~~

不过还好,咱们Java程序员不太考虑这些~~

Q12:那为啥不给前端??

其实前端也分广义的前端和狭义的前端

广义的前端也包含 PC 端,基于 HTML/CSS/JS 这一套技术体系,也能构建 桌面图形化程序的~~

但是这样的做法还没成为业界的主流方案,因为还是存在一些缺陷

主流的方案,还是基于 C++ 或者基于 C# 相关的技术方案~~

Q13:前端回暖了~~

??可不敢乱说~~前端从来没凉过~~

不要光听公众号、短视频就怎样怎样~~

同样进大厂,走前端方向必然比Java方向要容易~~

当然,前端也有一堆烂摊子事儿~~

大家应该多去了解一些招聘网站,比网上听说的靠谱多了👇


大家都没有问题了,我再来提出一个问题:

当前服务器启动了,启动之后,客户端还没有呢,当然也没有请求发来啦~~

在客户端请求发过来之前,服务器里面的逻辑都在干啥呢??

阻塞等待!!阻塞在下面这行代码👇

java 复制代码
socket.receive(requestPacket);

receive 会触发阻塞行为:

客户端请求发来了,receive 才会返回

客户端的请求没来,receive 就一直阻塞了~~

上面的表格中也对应介绍过👇

|--------------------------------|---------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |

完整服务器代码
java 复制代码
package network;
/**Echo 回声~~
//我们在这里通过称为回显服务器~~
//客户端给服务器发一个数据(请求)
//服务器返回一个数据(响应)
//回显服务器:请求是啥,响应就是啥~~
//真实的额服务器,请求和响应是不一定的~~
//但是当前阶段,先不考虑那些,先把API用起来,写个回显~~
 */
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;//net:网络
import java.net.SocketException;

public class UdpEchoServer {
    private DatagramSocket socket=null;//此处的private可省略
    //提供一个构造方法
    public UdpEchoServer(int port) throws SocketException {
        //SocketException:网络编程中常见的异常
        //指定了一个固定端口号,让服务器来使用
        socket=new DatagramSocket(port);
    }
    public void start() throws IOException {
        //启动服务器
        System.out.println("服务器启动");
        //对于服务器来说,客户端啥时候发请求,发多少个请求,我们无法预测
        //因此服务器中通常都需要有一个死循环,持续不断的尝试读取客户端的请求数据~~
        //即 7×24 小时运行~~
        while(true){
            //循环一次,就相当于处理一次请求
            //处理请求的过程,典型的服务器都是分成三个步骤的
            //1.读取请求并解析
            //  DatagramPacket 表示一个 UDP 数据报,此处传入的字节数组,就是保存 UDP 的载荷部分
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);//往上抛IOException异常
            //  把读取到的二进制数据,转成字符串,只是构造有效的部分:getData()拿到字节数组,getLength()拿到有效长度
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());

            //2.根据请求,计算响应(服务器最关键的逻辑)
            //  但是此处我们写的是回显服务器,这个环节相当于省略了,请求发啥就返回啥
            String response=process(request);

            //3.把响应返回给客户端
            //  根据 response 构造 DatagramPacket,发送给客户端
            //  此处不能使用 response.length(), 因为 response.length() 返回的是String中字符的个数
            //  而 response.getBytes().length 返回的是String中字节的个数
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            //  此处还不能直接发送,因为 UDP 协议自身没有保存对方的信息(不知道发给谁)
            //  需要指定 目的IP 和 目的端口
            socket.send(responsePacket);

            //4.打印一个日志
            System.out.printf("[%s:%d] req: %s, resp: %s\n",
                    requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }
    //后续如果要写别的服务器,只修改这个地方就好了
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        //启动服务器
        UdpEchoServer server=new UdpEchoServer(9090);
        server.start();
    }
}

UDP Echo Client(客户端)

上面编写了回显服务器的服务端,接下来编写回显服务器的客户端,二者是配合工作的~~

java 复制代码
package network;

import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoClient {
    private DatagramSocket socket=null;

    //UDP 本身不保存对端的信息,就在自己的代码中保存一下
    private String serverIp;
    private int serverPort;

    //和服务器不同,此处的构造方法是要指定服务器的地址(IP和端口)
    public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        this.serverIp=serverIp;
        this.serverPort=serverPort;
        //这里一定不能填写 serverPort,必须使用无参数版本~~
        //因为 serverIP 是目的IP,serverPort 是目的端口
        //源 IP:客户端所在的主机IP,源端口:随机搞一个端口(操作系统分配的空闲端口)
        socket=new DatagramSocket();
    }
}

写到这里,如果非要写个固定端口,改成类似下面这样👇,行不行呢?

java 复制代码
socket=new DatagramSocket(9111);

也不是完全不行,但是非常不推荐客户端使用固定端口~~

比如我改行去陕科大六餐厅盘个档口(18号)去卖熏肉大饼~~

陕科大六餐厅,就是服务器的 IP 地址

18号档口,就是服务器的端口号~~

在我往学校门口发传单的时候,传单上一定要印有我这里的地址和端口号~~

那问题就来了,我总不能第一天在18号档口,第二天去别的档口吧~~

那我老是换档口,之前的传单不就白发了吗?

所以我的端口号,也就是服务器的端口号是不能改变~~

不一会儿,真有同学来了~~

对我说,来一份猪肉的熏肉大饼,少葱,不要辣~~

我说,你先找个地方坐一下,我做好了给你端过去~~

这个时候,同学坐的地方,一定是"随机"的位置~~

因为在食堂,平时固定坐的位置,可能有人了,你只能随机坐(随机的找一个空闲的地方坐)~~

答疑:端口还会有竞争啊??

对的,端口号是区分同一个主机的不同的应用程序的

同一时刻,不能有两个程序使用同一个端口~~

操作系统中,一个程序尝试关联一个非空闲的端口,就会关联失败,抛异常~~

所以如果客户端是固定端口,很可能客户端运行的时候,这个端口被别的程序占用,就会使得当前这个程序运行失败!!

因为客户端是在用户手里,你无法控制你的用户电脑上都运行啥样的程序~~

一旦关联失败,用户也不觉得是他的其他应用程序抢了你的端口号,他也不懂,只会去赖你的代码有Bug~~

而服务器是在程序员手里的,就算出现端口冲突,程序员也会方便处理~~


我们继续写,写到这里会发现报错👇

因为没有提供这个版本的构造方法,需要将 serverIP 进行转换,将 serverIP 转换成 InetAdress 的结构体👇并且为其添加异常处理

java 复制代码
    //通过 start 启动客户端程序
    public void start() throws UnknownHostException {
        Scanner scanner=new Scanner(System.in);
        //1.从控制台读取用户输入的内容
        System.out.println("请输入要发送的内容:");
        String request=scanner.next();
        //2.把请求发送给服务器,需要构造 DatagramPacket 对象
        //  构造过程中,不光要构造载荷,还要设置服务器的 IP 和 端口号
        DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
                InetAddress.getByName(serverIp),serverPort);
    }

构造请求的数据报:

1.载荷:和之前服务器的差不多~~

2.目的IP 和 目的端口:

java 复制代码
InetAddress.getByName(serverIp)

serverIP 是按照字符串的方式来构造的,形如👇

给人看的,在计算机里通常会对它进行一个转换,通过 getByName 构造成一个在Java中能够识别的对象,转换之后再传进去就能构造出这个对象了

闲聊:其实这里的封装也不够彻底,导致还得咱们手动进行转换~~

但是相比操作系统原生的API,已经好很多了

隔壁C++的同学学的 socket api,用起来更费劲儿~~

相比之下,Python 的 socket api 是封装的更好的~~

当前这个就属于把 IP 和 端口 分别来构造~~

换句话说,当前这个对象的构造,我们已经介绍三种构造方式了:

1.只传数组的长度

2.数组的长度+完整的对象

3.IP 和 端口 分开来构造


把对象构造好了之后,就可以发送数据包了等一系列操作了~~👇

java 复制代码
    //通过 start 启动客户端程序
    public void start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        //1.从控制台读取用户输入的内容
        System.out.println("请输入要发送的内容:");
        String request=scanner.next();
        //2.把请求发送给服务器,需要构造 DatagramPacket 对象
        //  构造过程中,不光要构造载荷,还要设置服务器的 IP 和 端口号
        DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
                InetAddress.getByName(serverIp),serverPort);
        //3.发送数据报
        socket.send(requestPacket);
        //4.接收服务器的响应
        DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);//4096这个长度可以自己指定
        socket.receive(responsePacket);
        //5.从服务器读取的数据进行解析,打印出来
        String response=new String(responsePacket.getData(),0,responsePacket.getLength());
        System.out.println(response);
    }

然后让这整个的过程循环多次,给它套上一个循环~~

java 复制代码
    //通过 start 启动客户端程序
    public void start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        while(true){
            //1.从控制台读取用户输入的内容
            System.out.println("请输入要发送的内容:");
            //输入空行就结束(本质是读前校验)
            if(!scanner.hasNext()){
                break;
            }
            String request=scanner.next();
            //2.把请求发送给服务器,需要构造 DatagramPacket 对象
            //  构造过程中,不光要构造载荷,还要设置服务器的 IP 和 端口号
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            //3.发送数据报
            socket.send(requestPacket);
            //4.接收服务器的响应
            DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);//4096这个长度可以自己指定
            socket.receive(responsePacket);
            //5.从服务器读取的数据进行解析,打印出来
            String response=new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);
        }

再加上一个 main 方法👇

java 复制代码
    public static void main(String[] args) throws IOException {
        //创建客户端并指定服务器的 IP 和 端口
        UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }

另一方面,127.0.0.1 这是个特殊的 IP,叫 回环 IP,表示当前这个主机

无论你主机的 IP 真实是啥,都可以使用 127.0.0.1 代替,类似于 this

由于此时,客户端和服务器在同一个主机上,就可以使用 127.0.0.1 来访问

如果是不同主机,就需要填写其他的 IP 了

完整客户端代码
java 复制代码
package network;

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

public class UdpEchoClient {
    private DatagramSocket socket=null;

    //UDP 本身不保存对端的信息,就在自己的代码中保存一下
    private String serverIp;
    private int serverPort;

    //和服务器不同,此处的构造方法是要指定服务器的地址(IP和端口)
    public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        this.serverIp=serverIp;
        this.serverPort=serverPort;
        //这里一定不能填写 serverPort,必须使用无参数版本~~
        //因为 serverIP 是目的IP,serverPort 是目的端口
        //源 IP:客户端所在的主机IP,源端口:随机搞一个端口(操作系统分配的空闲端口)
        socket=new DatagramSocket();
    }

    //通过 start 启动客户端程序
    public void start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        while(true){
            //1.从控制台读取用户输入的内容
            System.out.println("请输入要发送的内容:");
            //输入空行就结束(本质是读前校验)
            if(!scanner.hasNext()){
                break;
            }
            String request=scanner.next();
            //2.把请求发送给服务器,需要构造 DatagramPacket 对象
            //  构造过程中,不光要构造载荷,还要设置服务器的 IP 和 端口号
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            //3.发送数据报
            socket.send(requestPacket);
            //4.接收服务器的响应
            DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);//4096这个长度可以自己指定
            socket.receive(responsePacket);
            //5.从服务器读取的数据进行解析,打印出来
            String response=new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);
        }
    }
    public static void main(String[] args) throws IOException {
        //创建客户端并指定服务器的 IP 和 端口
        UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

回显服务器与客户端的交互

结合我们写的回显服务器和客户端,交互效果如下👇

回显服务器与客户端的交互

答疑

Q1:不用联网也能跑吧?

此时客户端和服务器在同一个主机上,是否联网都可以的

不同主机上就必须联网了~~

Q2:这个是局域网吗??

不是,这个是同一台主机,相当于你自己发自己收,这还不涉及到网络编程的核心部分,核心部分在于跨主机通信,这是我们更希望看到的效果!!当前只是自己跟自己说话,没啥意思~~

Q3:能否让你们的电脑运行客户端,连我这个电脑的服务器??

能,又不能~~(有很多限制)

如果你能抱着电脑来到我的大学12416宿舍,连上相同的WiFi/网线,咱们处于同一个局域网,就可以~~

如果是你在家里/别的学校,是连不上我这个服务器的~~

Q4:那为啥不行??

后面详细讲,主要是 NAT 机制在搞鬼~~

咱们也是有办法应对的~~使用云服务器~~

我可以把程序部署到云服务器上,大家就可以访问到云服务器了~~

闲聊:云服务器~~

起因得从双十一说起~~

2012年11月11日

淘宝的第一次双十一优惠力度非常大(5折)

在双十一这一天,淘宝的访问量非常大~~直接把系统给干爆了!!!

阿里为了筹备2013年的双十一,引入了大量的服务器~~来应对双十一高峰的情况~~

高峰这时候,这些机器是用上了,但是过了双十一了,访问量降下来了,搞的这么多机器,就用不上了,都闲置了

这时候,阿里就选择了"租给其他人",这种形式就称为"云服务器",也就诞生了 阿里云 这样的部门~~

你还真别说,这种模式还挺香~~

因为2013年正好又赶上移动互联网发力,"全民创业潮"中小公司特别多~~

很多公司都是有这种需求的

在有云服务器之前,自建机房成本是非常高的~~

要防尘、防静电、防潮、控制噪音、备用电源、控制温度、搭配场地、运维人员......

中小公司搞,成本非常高~~

后来华为、腾讯就纷纷跟进~~

其实华为早就开始搞云了,只不过一直是内部使用,没有放出来~~

2012年的时候,华为办公都是"云端"办公~~(现在所谓的"云电脑")

但是还是被阿里抢了先手~~

Q5:搞个虚拟机呢??

"如能"~~

取决于虚拟机的网络是怎么设置的

虚拟机网络设置规格主要有三种典型:

1.桥接网络(可以)

2.NAT 网络(有的虚拟机软件可以,有的不行)

3.Host Only 网络(不可以)

云服务器

后面课程中也会要求大家来买云服务器,自己使用~~

就可以把自己做的项目部署到云服务器上,别人就能访问了

尤其是面试的时候,talk is cheap , show me the code~~

面试绝杀现场给面试官演示的~~

答疑

Q1:贵不贵??

学生买,非常便宜

一个月10块钱左右(赶上大促,还能更便宜)

10块买不了吃亏、10块买不了上当、10块买不了老妹陪你浪,但10块能买个云服务器乱杀~~

要是觉得贵,甚至可以2-3个同学合伙买一个机器~~平均1个月3块

如果还嫌贵,阿里云/腾讯云 不定期能放出 白嫖的机器(数量有限),一般能白嫖半年~~

但现在我们的课程还不着急,后续用到会说的~~

Q2:GPU 云服务器怎么样呀??

老贵了,不划算,不如搞一个二手的 2080ti 这种~~

Q3:云服务器有好差之分吗?

当然有啊,但是咱们的话,挑便宜的买就行~~

Q4:买了服务器还得买个域名吧??

买不买都行,不影响~~

Q5:chat域名卖了几千万好像

这属于域名理财了,相当于买彩票~~非常的不靠谱~~

Q6:这些服务器都是Linux吗?

公司里都是Linux,虽然可以装 Windows,但是没有公司真的这么搞~~


接下来的操作,小伙伴们都不必深究什么含义,后面时机到了自然会讲~~

当前只是操作一下,让大家先看看~~

1.把代码打 包(jar)

闲聊:我的世界的 mod 都是 jar

确实,这是Java程序发布的主流方式,IDEA 的插件也是 .jar

2.上传到云服务器

(Linux系统,通过命令行操作)

3.启动服务器

java -jar jar 包名(类似于 IDEA 中点一下运行按钮~~)

4.启动客户端(本地的电脑)

只需在客户端把连接的 服务器的IP 地址,改成云服务器的 IP

我们能发现,一个服务器能够同时给多个客户端提供服务~~

我们也可以通过 IP地址查询 找到大家的位置👇

这个 IP 地址也会变,我们后续会讲~~

阶段性小结

DatagramSocket=>网卡

编写网络代码时,无论写客户端还是服务器,首当其冲第一步,先有 socket 对象来操作网卡

DatagramPacket=>数据报

UDP 是面向数据报的协议

发送接收的时候,以数据报作为基本单位

回显服务器(Echo Server)

1.创建 DatagramSocket 进行初始化

2.先有 while(true) 循环保证服务器 7×24持续运行

1)读取请求并解析

receive 时通过输出型参数来完成请求的获取

为了后续处理方便,再把字节数组取出来转化成 String,以 String 作为请求对象作后续处理

2)根据请求计算响应(最关键的逻辑)

正常这里需要花费大量的代码来写,但是回显服务器就相对简单很多~~请求是啥响应就是啥

3)把响应返回给客户端

通过 DatagramPacket 把响应构造进对象中,注意长度长度取字节数组的长度,而不是 String 的长度,String 的长度单位是字符,而字节数组的长度单位是字节

还需要指定目标地址、目标端口,在 send 中手动指定

客户端

与服务器的逻辑相对应

1.从控制台读取用户输入的内容

2.把请求发送给服务器,需要构造 DatagramPacket 对象

发送的时候也要提供目的IP 和 目的端口号

3.发送数据报

此时 服务器就能 receive 到

4.接收服务器的响应

就是在接收服务器返回的数据报(send(responsePacket))

5.从服务器读取的数据进行解析,打印出来

UDP Dict Server(翻译服务器)

其实我们当前写的这个回显服务器,你发啥我就返回啥,没啥意思~~

一个真实的服务器,通常要带有业务逻辑,要能解决实际问题~~

下面我们写一个 翻译服务器(中译英),只需要重写 process 即可

完整代码
java 复制代码
package network;

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

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: CoderYanger
 * Date: 2026-06-14
 * Time: 17:55
 */
//中译英服务器
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("小狼", "wolf");
        dict.put("小兔子", "rabbit");
        dict.put("小牛", "cow");
        dict.put("小羊", "sheep");
        dict.put("小鸡", "chicken");
        dict.put("小鸭子", "duck");
    }
    //只需要重写一下 process() 方法
    //UdpEchoServer 处对应的 process() 方法要把private改成public,否则不能被重写
    @Override
    public String process(String request) {
        //查字典:没找到就返回"词典中无此单词"
        return dict.getOrDefault(request, "词典中无此单词");
    }
    public static void main(String[] args) throws IOException {
        UdpDictServer server=new UdpDictServer(9090);
        server.start();
    }
}
交互演示

中译英服务器与客户端的交互

答疑

Q1:这个可以导现成的词典包吗??(好奇)

理论上是有的,需要查一下

Q2:利威尔:把牛津cv过来,老厚了~~

对于计算机来说,不算什么~~

假设一个词条,1kb这么大(往大了算了)

100w个词条×1kb=>1GB(so easy~~)

相关推荐
艾莉丝努力练剑1 小时前
【Linux网络】多路转接select
java·linux·运维·服务器·网络·tcp/ip
Cx330❀1 小时前
【Linux网络】从零定制应用层协议:黏包问题、全双工缓冲区与 Jsoncpp 序列化深度解析
linux·运维·服务器·开发语言·网络·c++·人工智能
Benszen1 小时前
云计算基础-5:Linux 重定向与管道
linux·运维·服务器
lazy H1 小时前
IDEA 如何配置 JDK?项目 SDK 报错解决方法
java·ide·后端·学习·intellij-idea
吴声子夜歌1 小时前
SQL经典实例——处理数字
java·数据库·sql
YHHLAI1 小时前
LeetCode 136.只出现一次的数字 | 从遍历统计到位运算极致优化
算法·leetcode·职场和发展
浮午1 小时前
Agentic RAG:从检索增强生成到智能体驱动的问答系统
面试
lang201509281 小时前
Java SAX 流式解析全解:从原理到 EasyExcel 实战
java·前端·javascript
艾莉丝努力练剑1 小时前
【Linux网络】五种IO模型与非阻塞IO
linux·运维·服务器·开发语言·网络·tcp/ip
VidDown1 小时前
视频协议传输全解析:从 HTTP/HTTPS 到 HLS/DASH 的完整旅程
javascript·网络·http·https·编辑器·音视频·视频编解码