【Java成王之路】EE初阶第十一篇:(网络原理) 1

上节回顾

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

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

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

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

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

【Java成王之路】EE初阶第十一篇:(网络原理) 1_第1张图片 服务器代码实现

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

public class CalcServer {
    private DatagramSocket socket = null;

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

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 1. 读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            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());
            socket.send(responsePacket);
            // 4. 打印日志
            String log = String.format("[%s:%d] req: %s; resp: %s", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
            System.out.println(log);
        }
    }

    // process 内部就要按照咱们约定好的自定协议来进行具体的处理!
    private String process(String request) {
        // 1. 把 request 还原成操作数和运算符
        String[] tokens = request.split(";");
        if (tokens.length != 3) {
            return "[请求格式出错!]";
        }
        int num1 = Integer.parseInt(tokens[0]);
        int num2 = Integer.parseInt(tokens[1]);
        String operator = tokens[2];
        // 2. 进行具体的运算了
        int result = 0;
        // 完全可以换成 switch
        if (operator.equals("+")) {
            result = num1 + num2;
        } else if (operator.equals("-")) {
            result = num1 - num2;
        } else if (operator.equals("*")) {
            result = num1 * num2;
        } else if (operator.equals("/")) {
            result = num1 / num2;
        } else {
            return "[请求格式出错! 操作符不支持!]";
        }
        return result + "";
    }

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

 客户端代码实现

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

public class CalcClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    public CalcClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        this.socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 让用户进行输入
            System.out.println("请输入操作数 num1: ");
            int num1 = scanner.nextInt();
            System.out.println("请输入操作数 num2: ");
            int num2 = scanner.nextInt();
            System.out.println("请输入运算符(+ - * /): ");
            String operator = scanner.next();
            // 2. 构造并发送请求
            String request = num1 + ";" + num2 + ";" + operator;
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            // 3. 尝试读取服务器的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 4. 显示这个结果
            System.out.println("计算结果为: " + response);
        }
    }

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

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

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

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

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

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

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

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

大体分成两类:

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既能读,也能写

传输层的概念

端口号 

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

【Java成王之路】EE初阶第十一篇:(网络原理) 1_第2张图片

举例:

当我们开发广告的时候,

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

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

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

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

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

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

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

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

在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协议必须得

理解协议报文格式.

【Java成王之路】EE初阶第十一篇:(网络原理) 1_第3张图片 四个字段分别都是啥呢?

第一个字段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%的就能进行校验

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

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

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

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

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

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

你可能感兴趣的:(网络,服务器,运维,java,开发语言)