• 【Java成王之路】EE初阶第十一篇:(网络编程) 5


    上节回顾

    TCP socket(核心:要掌握的两个类,Serversocket,socket)

    回显服务器(无法支持多个客户端并发执行)

    多线程回显服务器(针对每个连接(每个客户端)创建一个线程)

    线程池回显服务器(避免频繁创建/销毁线程) 

    接着上一篇五层协议继续写.

     服务器代码实现

    1. import java.io.IOException;
    2. import java.net.DatagramPacket;
    3. import java.net.DatagramSocket;
    4. import java.net.SocketException;
    5. public class CalcServer {
    6. private DatagramSocket socket = null;
    7. public CalcServer(int port) throws SocketException {
    8. socket = new DatagramSocket(port);
    9. }
    10. public void start() throws IOException {
    11. System.out.println("服务器启动!");
    12. while (true) {
    13. // 1. 读取请求并解析
    14. DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
    15. socket.receive(requestPacket);
    16. String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
    17. // 2. 根据请求计算响应
    18. String response = process(request);
    19. // 3. 把响应写回到客户端
    20. DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
    21. requestPacket.getSocketAddress());
    22. socket.send(responsePacket);
    23. // 4. 打印日志
    24. String log = String.format("[%s:%d] req: %s; resp: %s", requestPacket.getAddress().toString(),
    25. requestPacket.getPort(), request, response);
    26. System.out.println(log);
    27. }
    28. }
    29. // process 内部就要按照咱们约定好的自定协议来进行具体的处理!
    30. private String process(String request) {
    31. // 1. 把 request 还原成操作数和运算符
    32. String[] tokens = request.split(";");
    33. if (tokens.length != 3) {
    34. return "[请求格式出错!]";
    35. }
    36. int num1 = Integer.parseInt(tokens[0]);
    37. int num2 = Integer.parseInt(tokens[1]);
    38. String operator = tokens[2];
    39. // 2. 进行具体的运算了
    40. int result = 0;
    41. // 完全可以换成 switch
    42. if (operator.equals("+")) {
    43. result = num1 + num2;
    44. } else if (operator.equals("-")) {
    45. result = num1 - num2;
    46. } else if (operator.equals("*")) {
    47. result = num1 * num2;
    48. } else if (operator.equals("/")) {
    49. result = num1 / num2;
    50. } else {
    51. return "[请求格式出错! 操作符不支持!]";
    52. }
    53. return result + "";
    54. }
    55. public static void main(String[] args) throws IOException {
    56. CalcServer server = new CalcServer(9090);
    57. server.start();
    58. }
    59. }

     客户端代码实现

    1. import java.io.IOException;
    2. import java.net.*;
    3. import java.util.Scanner;
    4. public class CalcClient {
    5. private DatagramSocket socket = null;
    6. private String serverIp;
    7. private int serverPort;
    8. public CalcClient(String serverIp, int serverPort) throws SocketException {
    9. this.serverIp = serverIp;
    10. this.serverPort = serverPort;
    11. this.socket = new DatagramSocket();
    12. }
    13. public void start() throws IOException {
    14. Scanner scanner = new Scanner(System.in);
    15. while (true) {
    16. // 1. 让用户进行输入
    17. System.out.println("请输入操作数 num1: ");
    18. int num1 = scanner.nextInt();
    19. System.out.println("请输入操作数 num2: ");
    20. int num2 = scanner.nextInt();
    21. System.out.println("请输入运算符(+ - * /): ");
    22. String operator = scanner.next();
    23. // 2. 构造并发送请求
    24. String request = num1 + ";" + num2 + ";" + operator;
    25. DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
    26. InetAddress.getByName(serverIp), serverPort);
    27. socket.send(requestPacket);
    28. // 3. 尝试读取服务器的响应
    29. DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
    30. socket.receive(responsePacket);
    31. String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
    32. // 4. 显示这个结果
    33. System.out.println("计算结果为: " + response);
    34. }
    35. }
    36. public static void main(String[] args) throws IOException {
    37. CalcClient client = new CalcClient("127.0.0.1", 9090);
    38. client.start();
    39. }
    40. }

    所谓的自定义协议,一定是开发之前,就要约定好的

    开发的过程中,就需要让客户端和服务器之间们都能够严格遵守协议约定好的格式.

    此处约定格式的方式有很多种,当前咱们是使用一个最简单粗暴的方式来约定(直接实用文本+分隔符)

    如果解决简单问题,那还行,如果是复杂问题,就难搞了.

    如果是复杂问题:假设传输的请求和响应中,各自有几十个字段....有的字段可能是"可选的"(可有可无)

    实际开发中,如何来约定自定义协议呢?

    除了刚才这种简单粗暴的文本+分隔符的方式,还有那些更好的方式?

    大体分成两类:

    1.文本格式(把请求和响应当成字符串来处理,处理的基本单位是字符)

    文本格式常见的方式:xml,json...

    2.二进制格式(把请求响应当成二进制数据处理,处理的基本单位是字符)

    二进制方式:protobuffer,thift.....

    xml:

    格式化组织数据的方式.

    针对上面刚写的场景使用xml来设置协议大概样子:

    请求:

       10

       10

       +

    响应:

       30

    xml:把数据组成了一个结构化的数据

    整个xml是由"标签"构成的

    标签,是成对出现的

    形如开始标签结束标签

    开始标签和结束标签之间的东西就是值

    这种格式,其实很常见,不仅仅可以用于自定义协议(不仅仅可以用于网络传输)

    咱们很多Java中涉及到的配置之类的,也经常会使用xml这样的格式来组织.

    json也是非常有特点的格式

    请求:

    {

        num1: 10,

        num2: 20,

        operator: "+"

    }

    响应:

    {

    result: 30

    }

    这里的json是键值对结构.键和值之间,使用 : 分割,键值对之间,使用 逗号 分割.

    整体最外面包含一个{}

    格式:也是能够结构化的组织数据

    json也是有一些配套的的第三方库来帮助我们构造和解析

    自定义协议,其实是一个很简单的事情.

    只要约定好请求和响应是详细即可.(越详细越好,要把各种细节都交代到,能够很好的表示当前的信息)

    咱们可以自己来约定格式,也可以基于xmlhejson来约定何时,还可以通过一些其他的二进制的方式来约定格式...... 

    传输层

    负责端对端的数据传输

    只考虑起点和终点,不考虑中间过程

    传输层由于是操作系统内核实现的,因此谈到的传输层协议,一般都是指现成的一些协议.很少会涉及"自定制"

    UDP,TCP都是属于传输层的协议.

    UDP

    1.无连接

    2.不可靠

    3.面向数据报

    4.全双工

    TCP

    1.有连接

    2.可靠

    3.面向字节流

    4.全双工

    有连接:socket创建好了之后,还需要建立连接,连接建立完了,在通过accept获取到连接才能进行读写数据

     无连接:socket创建好之后就可以立即尝试读写数据了

    面向数据报:读写数据都是以DatagramPacket为单位进行的

    面向字节流:读写数据直接以byte[]为单位.

    全双工:一个socket既能读,也能写

    传输层的概念

    端口号 

    端口号的用途:表示一个进程,就可以区分出当前收到的数据要交给哪个进程来处理.

    举例:

    当我们开发广告的时候,

    首先会让服务器提供一个"业务端口"

    通过这个端口,提供一些广告搜索服务.(上游客户端,就可以通过这个端口来请求获取到广告数据) 

    其次还会让服务器提供一个"调试端口"

    服务器运行过程中,其实涉及到很多很多的数据.有时候为了定位一些问题,就需要查看到这些内存数据.通过这个调试端口给服务器发送一些调试请求,于是服务器就能返回一些对应的结果.

    为什么这么麻烦,直接拿调试器,来个断点啥的不就行了吗?

    如果拿调试器断住程序,此时这整个进程是处在一个"阻塞"的状态中,这就意味着这个服务器就无法响应正常的业务需求了.

     通常情况下,两个进程无法绑定掉同一个端口号!!

    有的特殊情况下,可以做到!

    在Linux中,

    先让进程,绑定一个端口,接下来,通过fork这个系统调用,把进程的PCB复制一份,得到一个新的,

    "子进程"

    由于端口是关联在socket上,而socket是一个文件,这个文件在文件描述符表中.

    而文件描述符表又是PCB的一部分

    fork复制PCB,也就把文件描述符表给继承下来了.也就顺带的把这样的端口号的关联关系也给继承过来......

    这种场景在Java中基本不会涉及.....

    端口号是一个整数.

    是一个两个字节的整数

    0~65535(没有负数)

    这么多端口我们能随便用嘛?

    其实也不是,在这些端口里面有些端口咱们程序猿可以随便用,有些不能随便用.

    0-1023这些端口,称为"知名端口"

    当前已经有很多现成的应用层协议了.

    就给这些现成的应用层协议,已经分配了一些端口号了

    举例:

    80 一般就是给HTTP使用

    22 一般给SSH使用

    21 一般给FTP使用

    23 一般给telnet使用

    443 一般给HTTPS使用

    .......

    针对这些知名端口号,咱们在实际开发的时候也不一定非得要严格遵守.

    例如:

    tomcat,也是以一个HTTP服务器,但是它使用的默认端口是8080,而不是80.

    但是咱们自己写的一些服务器,最好不要使用知名端口号

    另外的一些系统上,比如linux,如果进程要绑定知名端口号,往往需要管理员权限.

    咱们自己写个服务器,使用哪个端口,随你喜欢,只要尽量避开知名端口号,并且在65535范围之内即可.

    UDP协议

    要想了解好UDP协议必须得

    理解协议报文格式.

     四个字段分别都是啥呢?

    第一个字段16位(bit位)源端口号 (相当于发件人的姓名)

    第二个字段16位目的端口号(相当于收件人姓名)

    第三个字段16位UDP长度(长度指的是整个UDP数据报的长度(报头+载荷),使用两个字节的数据来表示.单位是字节)

    2个字节能表示的数据范围:

    0-65535 byte

    一个UDP数据报,最大就是64KB

    64K这个长度是长还是短?

    在现代互联网看起来64K太小了.

    198x,199x那个时代,64K就不小了.

    在实际开发中,如果使用UDP来传输数据,一定要警惕大的报文.

    如果报文长度超过64K,此时就可能丢失一部分数据.

    第四个字段校验和(网络上传输的数据,是可能会出现一些问题的.网络上的数据本质都是一些0/1 bit流.这些bit流都是通过光信号或者电信号来表示的.如果传输过程中,收到一些干扰,就容易出现"比特翻转情况,也就是(0变1,1变0)")

    校验和其实就是为了验证,看当前的数据是否出现问题了.

    校验和也是一种信息上的冗余.

    校验和也不一定100%的就能进行校验

    如果校验和正确,也不能确保数据一定对.

    但是如果校验和不正确,能说明数据一定是错的

    校验和更多的用处,是"证伪".

    校验和往往是根据原始数据的内容来生成的.不同的内容,生成校验和也就不一样.

    这个时候,一旦数据发生了变化,校验和也就不一样了.

    就可以通过校验和来判定当前的数据是否发生了变化了.

  • 相关阅读:
    整合mysql多个bool值字段,用&查询
    POJ 3481:双端队列 ← 数组模拟
    AtomicInteger原理
    如何在Google App Engine上构建一个简单的应用
    【读书笔记】【Effective C++】模板与泛型编程
    Spark创建空的df
    计算机行业已经进入寒冬?云计算帮你解决就业难题!
    【我的电赛日记(完结)---2021全国大学生电子设计竞赛全国一等奖】A题:信号失真度测量装置
    计算机毕业设计(附源码)python在线药物配送系统
    mysql中的这些日志,你都知道吗?
  • 原文地址:https://blog.csdn.net/m0_64397675/article/details/126008450