认识网络(一)

一、网络初识

1.网络协议

        

        形如上面图示中的一样,网络通信指的是两个主机之间,进行数据传输的过程,但是,实际生活中咱们的网络通信是这样的吗?

        其实并不是,主机A和主机B之间是有着很复杂的一段过程的,主机和主机之间会经历类似如下的一个过程:

        数据为什么能准确的从主机A送达到主机B呢?

        这里我们主要依靠的就是“网络协议”,通过一些既定的规则去编排数据,再用相同的规则去解析数据,那么最终数据就能够完整地被传输。

2.协议分层

        为什么要引入协议分层?

        比如说,一个公司是存在着上下级关系的,普通员工是不能够直接去跨级汇报事情给公司董事长的,必须一级一级的汇报,否则董事长就忙不过来了,那么网络协议也是类似,为了避免跨层级调用引起的混乱,人为的将协议进行分层,上层协议调用下层协议,下层协议服务上层协议,降低耦合,提高效率。

        协议分层分为两种:

        1.OSI七层网络协议模型(只出现在教科书中,了解即可)

        

         2.TCP/IP五层网络模型(重点)

        

        物理层:描述的网络通信中的一些基础设施需要遵守的规范(如网线、网口等的样子)

        数据链路层:描述相邻节点之间,数据如何传输

        网络层:路径规划(如上海->西安,有很多不同的路线走法,网络层这里就需要规划该走哪条路线)

        传输层:描述数据从哪里来,到哪里去

        应用层:描述数据用来干什么用

3.数据传输的基本流程

        数据传输的核心步骤有两个:1.封装        2.分用

        以QQ发信息为例:

        用户A给用户B发信息


        发送方的情况:

        1.应用层:

        QQ应用程序,从输入框获取到你所输入的信息,构造成应用层数据报(根据应用层协议),在这之后,引用程序就会调用传输层提供的接口,把数据报交给传输层处理。

        2.传输层:

        传输层的协议有很多,最主要的是TCP和UDP协议,这里以UDP为例

        应用层传到传输层的数据,通过UDP协议,生成一个UDP数据报

        UDP并不关心应用层发来的数据里面有什么,都是些什么内容,只是把应用层的数据当作一个字符串,构造出一个UDP数据报,这个数据报包含两个部件,分别是UDP报头和UDP载荷。

        UDP报头包含两个主要信息:1.源端口        2.目的端口

        传输层再进一步将UDP数据报交给网络层

        3.网络层

        网络层最主要的协议是IP协议

        IP协议根据自己的格式,构造出一个IP数据报

        IP协议同样也不关心传输层发来的是什么数据,只是在数据的前面拼接上一个IP报头

        IP报头包含两个最主要信息:1.源IP        2.目的IP

        此时我们可以引出网络通信的“五元组”:1.源IP        2.源端口        3.目的IP        4.目的端口        5.协议类型

        接下来,我们的网络层继续将数据上传给数据链路层                

        4.数据链路层

        最重要的协议:以太网(平时上网需要插的一个网线)

        以太网又会针对IP数据报进一步的进行封装,再添加上帧头和帧尾

        最后继续将IP数据报传给物理层

        5.物理层

        以太网的数据报本质上都是二进制的数据,通过硬件设备,如网卡等,将这些二进制数据转换成光信号/电信号/电磁波等。

        到这里,主机A就完成了发送过程


        接受方的情况(先不考虑中间过程)

        1.物理层

        一些硬件设备,如网卡等接收到电信号/光信号/电磁波等,再把这些信号进行解调,得到了一串二进制数据序列,也就是以太网数据报,这个数据被递交给上一层,数据链路层。

        2.数据链路层

        数据链路层的以太网协议就开始对数据进行解析,将帧头和帧尾解析,再将载荷交给上层,即网络层。

        3.网络层

        IP协议针对载荷进行解析,去掉IP报头,取出载荷,进一步交给传输层。

        4.传输层

        根据IP报头中的字段,就知道当前是一个UDP数据报,交给UDP处理,UDP针对数据报进行解析,去掉报头,取出载荷,再进一步交给应用程序。

        5.应用层

        UDP报头中,有一个字段是目的端口,目的端口找到关联的应用程序QQ,将数据交给它,QQ根据它的应用层协议,进行解析,再把这里的数据显示到界面上。

        这样完整的十个过程就王成了QQ通信。 

        主机A从上到下添加报头的过程就叫做封装

        主机B从上到下解析报头的过程就叫做分用

二、网络编程

        网络传输层协议主要有两个:

        1.UDP:不可靠传输,无连接,面向字符流,全双工

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

        1.UDP

        UDP主要有两个核心api:

        1)DatagramSocket

        在操作系统中,使用文件的概念去管理一些软硬件资源,如网卡等,表示网卡的这类文件,称为socket文件,要进行网络通信就必须有socket对象。

        

        了解一个类,我们先了解下它的构造方法,DatagramSocket提供了两个构造方法,一个指定了port端口号,一个没有指定,我们之前了解知道,网络通信五元组分别是:源端口、源IP、目的端口、目的IP、协议类型,客户端和服务器在进行发送信息的时候,就需要一个端口来进行对端传输。

        这里需要注意的是,客户端端口号系统会自动分配,服务器端口号需要自行指定。

        为什么要这样安排呢?

        一个客户端主机上,有很多的程序并发运行,我们并不能很明确知道某个端口是否是空闲的,而让系统自行去分配,这样更为的明智,而服务器是我们能够控制的,我们将服务器的程序安排好,就能很快找到空闲的端口了。举个例子:我在马路边开了个饭店,这个饭店的地址就必须是固定的且明确的,方便别人找到,客人来我这吃饭,它们坐的位置不是固定的,哪里空就坐哪里,饭店就相当于服务器,客人的位置就相当于客户端。

2)DatagramPacket

        DatagramPcket表示了一个数据报,代表了系统中设定的UDP数据报的二进制结构

        构造方法:

3)实例

        到目前为止,我们了解了UDP的两个核心api,那我们就要开始进行实例训练了,我们这里做一个简单的客户端-服务器,叫做“回显服务器”,客户端发送什么,服务器就返回什么,但是实际开发中,服务器返回值都是比较复杂的,这里只是简单的一个应用。

        服务端代码:

//回显服务器
//客户端发的请求是什么,服务器返回的响应就是什么
public class UdpEchoServer {
    public DatagramSocket socket = null;
    public UdpEchoServer(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);
            //设置日志,记录服务端数据变化
            System.out.printf("[%s:%d] req:%s,resp:%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }
    public String process(String response){
        return response;
    }

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

        客户端代码:

public class UdpEchoClient {
    public DatagramSocket socket = null;
    public String serverIp;
    public int port;
    //服务器的ip和端口传入
    public UdpEchoClient(String serverIp,int port) throws SocketException {
        this.serverIp = serverIp;
        this.port = port;
        //这个new操作,不再指定端口,由系统自动分配
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        while(true){
            //1.从客户端控制台输入内容
            System.out.print("->:");
            String request = scanner.next();
            //2.构造请求对象,传入服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),port);
            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 {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
        udpEchoClient.start();
    }
}

2.TCP

        TCP也同样提供了两个核心api:

        1)ServerSocket

        这个主要是给服务器使用。

        2)Socket

        这个不仅服务器可以使用,客户端也会用。

        这里两个api的构造方法可以在java官方文档中查看详情,这里主要不是为了讲解这些构造方法的,所以先跳过,主要需要知道的就是前两个构造方法。

3)实例

        TCP这里我们同样实现一个回显服务器,但是这里我们需要详解一些代码。

        服务端:

public class TcpEchoServer {
    public ServerSocket serverSocket = null;

    //这个操作用来绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            Socket clientSocket = serverSocket.accept();
            //需要使用多线程,否则会使一个服务器只能服务一个客户端的情况
            Thread t = new Thread(() -> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }

    //通过这个方法来处理一个连接的逻辑
    public void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来就可以读取请求,根据请求计算响应,返回响应三步走
        //Socket对象内部包含了两个字节流对象,可以把这两个字节流对象获取到,完成后续的读写操作
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //一次连接中,可能会涉及到多次请求/响应
            while(true){
                //1.读取请求并解析.为了读取方便,直接使用Scanner
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    //读取完毕,客户端下线
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //这个代码暗含一个约定,客户端发过来的请求,得是文本数据,同时,还得带有空白符(包括但不限于,换行,回车,空格等)做为分隔
                String request = scanner.next();
                //2.根据请求,计算响应
                String response = process(request);
                //3.把响应写回客户端,把OutputStream使用PrintWriter包裹一下,方便进行发数据
                PrintWriter writer = new PrintWriter(outputStream);
                //使用PrintWriter的println方法,把响应返回给客户端
                //此处使用println,而不是print,是为了给结尾加上\n,方便客户端读取响应,使用scanner.next读取
                writer.println(response);
                //PrintWriter内置了缓冲区,通过手动刷新缓冲区,确保数据真的通过网卡发出去了,而不是残留在内存缓冲区中
                writer.flush();
                //日志,打印当前的请求详情
                System.out.printf("[%s:%d] req:%s, resq:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
            }
        }finally {
            //clientSocket被反复创建,所以需要关闭
            clientSocket.close();
        }
    }
    public String process(String request){
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}
       a)针对服务端代码解析

        

        我们映入眼帘的第一个与UDP回显服务器不同的是这里,这是什么意思?accept了一个什么东西?

        我们之前就知道TCP是有连接传输,客户端和服务器建立好连接以后就会存入一个队列,而这个accept就是将存放在内核中的连接拿出来进行使用。

                第二个与UDP回显服务器不同的是这里,为什么使用多线程

        因为TCP是有连接的,如果单线程状态下,连续的两个客户端同时发送请求,就会有一个进入堵塞,但我们使用多线程的话,这样就一个服务器能同时处理多个客户端发送过来的请求。这里我们还可以进行优化,多线程中了解了线程池的概念,所以我们这里可以使用线程池来写,代码如下:

//创建一个不固定数量的线程池
    public ExecutorService service = Executors.newCachedThreadPool();
//启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            Socket clientSocket = serverSocket.accept();
            //需要使用多线程,否则会使一个服务器只能服务一个客户端的情况
//            Thread t = new Thread(() -> {
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
//            });
//            t.start();
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

        客户端:

public class TcpEchoClient {
    public Socket socket = null;

    // 要和服务器通信,就需要先知道,服务器所在的位置
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //这个new操作完成之后,就完成了tcp连接的建立
        socket = new Socket(serverIp,serverPort);
    }

    public void start() throws IOException {
        System.out.println("客户端启动了!");
        Scanner scannerConsole = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                //1.从控制台输入字符串
                System.out.print("->:");
                String request = scannerConsole.next();
                //2.把请求发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                //使用println带上换行,后续服务器读取请求,就可以使用scanner.next()来获取了
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应
                Scanner scannerNetwork = new Scanner(inputStream);
                String response = scannerNetwork.next();
                //4.把响应打印出来
                System.out.println(response);
            }
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值