《网络编程入门指南:1小时掌握UDP狂飙与TCP长跑的代码奥义》

一.什么是⽹络编程

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

二.网络编程中的基本概念

2.1发送端和接收端

在⼀次⽹络数据传输时:

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

接收端:数据的接收⽅进程,称为接收端。接收端主机即⽹络通信中的⽬的主机。

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

2.2请求与响应

⼀般来说,获取⼀个⽹络资源,涉及到两次⽹络数据传输:
• 第⼀次:请求数据的发送
• 第⼆次:响应数据的发送。

2.3客户端与服务端

服务端:在常⻅的⽹络数据传输场景下,把提供服务的⼀⽅进程,称为服务端,可以提供对外服务。
客⼾端:获取服务的⼀⽅进程,称为客⼾端。

三.Socket套接字

3.1Socket概念

Socket套接字,是由系统提供⽤于⽹络通信的技术,是基于TCP/IP协议的⽹络通信的基本操作单元。基于Socket套接字的⽹络程序开发就是⽹络编程

3.2Socket分类

Socket套接字主要针对传输层协议划分为如下三类:
流套接字:使⽤传输层TCP协议TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
以下为TCP的特点(细节后续再学习):
有连接

此处的连接本质上就是建立连接的双方,各自保存对方的信息,TCP 要想通信, 就需要先建立连接 (刚才说的, 保存对方信息),做完之后,才能后续通信(如果 A 想和 B 建立连接, 但是 B 拒绝了! 通信就无法完成!!!)
可靠传输
⾯向字节流

TCP 也是和文件操作一样,以字节为单位来进行传输
有接收缓冲区,也有发送缓冲区
⼤⼩不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是⽆边界的数据,可以多次发送,也可以分开多次接收。



数据报套接字:使⽤传输层UDP协议
UDP,即User Datagram Protocol(⽤⼾数据报协议),传输层协议。
以下为UDP的特点(细节后续再学习):
⽆连接

UDP 想要通信, 就直接发送数据即可~~不需要征得对方的同意,UDP 自身也不会保存对方的信息
不可靠传输
• ⾯向数据报

UDP 则是按照数据报为单位,来进行传输的
有接收缓冲区,⽆发送缓冲区
⼤⼩受限:⼀次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是⼀块⼀块的,发送⼀块数据假如100个字节,必须⼀
次发送,接收也必须⼀次接收100个字节,⽽不能分100次,每次接收1个字节。
原始套接字
原始套接字⽤于⾃定义传输层协议,⽤于读写内核没有处理的IP协议数据。我们不学习原始套接字,简单了解即可。

四.Socket的API如何使用

4.1UDP的socket的API使用

4.1.1DatagramSocket

在 Java 中就使用这个类,来表示系统内部的 socket 文件了,socket 其实也是操作系统中的一个概念,本质上是一种特殊的文件.Socket 就属于是把"网卡"这个设备,给抽象成了文件了往 socket 文件中写数据,就相当于通过网卡发送数据,从 socket 文件读数据, 就相当于通过网卡接受数据.至于怎么使用往下看代码中清楚的交代了如何使用。

4.1.2DatagramPacket

使用这个类,来表示一个 UDP 数据报!!UDP 是面向数据报的~~每次进行传输, 都要以 UDP 数据报为基本单位..至于怎么使用往下看代码中清楚的交代了如何使用。

五.UDP的简单的客户端/服务器

这个程序没有啥业务逻辑,只是单纯的调用 socket api.让客户端给服务器发送一个请求,请求就是一个从控制台输入的字符串.服务器收到字符串之后,也就会把这个字符串原封不动的返回给客户端, 客户端再显示出来.也就是回显服务器(echo server)

直接上代码!!!

这个是UDP服务器代码

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

public class UdpEchoServer {
    //创建一个DatagramSocket 对象,方便操作网卡的基础
    //DatagramSocket:绑定本机端口 → 发送/接收数据报
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        //这么写就是手动指定端口
        socket = new DatagramSocket(port);
        //这么写就是系统自动分配端口
        //socket = new DatagramSocket();
    }
    //通过这个方法启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!!!");
        //要保证服务器一直在运行
        while (true){
            //1.读取数据并解析
            // DatagramPacket:传输的数据报容器
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //完成receive之后,数据以二进制的形式存储到requestPacket
            //想要把数据显示出来,需要把二进制转换成字符串
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
            //2.根据请求计算响应
            //由于此处是回显服务器,请求是什么,响应就是什么
            String response  = process(request);
            //3.把响应写回客户端
            //写一个响应对象
            //向对象中构造刚才的数据,再通过send返回
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印一个日志,把这次数据交互的详情打印出来
            System.out.printf("[%s:%d] req = %s,resp=%s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }
    public String process(String requset) {
        return requset;
    }

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

    }
}

 下面是客户端的代码:

package network;

import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp = "";
    private int serverPort =0;

    public UdpEchoClient(String ip,int Port) throws SocketException {
        //创建这个对象,不能手动创建端口
        socket = new DatagramSocket();
        //由于UDP自身不会持有对端的信息,需要在应用程序里,把对端的情况记录下来
        //这里主要记录对端的IP和端口
        serverIp =ip;
        serverPort = Port;
    }
    public void start() throws IOException {
        System.out.println("客户端启动!!!");
        Scanner scanner =new Scanner(System.in);
        while(true){
            //1.从控制台读取数据,作为请求
            System.out.println("->");
            String request = scanner.next();
            //2.把请求的内容构造成DatagramSocket 对象,发送给服务器
            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);
            //4.把响应转换成字符串,显示出来
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.1.2.3",9090);
        client.start();

    }
}

由于客户端与服务器本质代码区别不太,下面主要讲解下服务器代码中的重点内容:

此时socket对象就能绑定到这个指定的端口

服务器和客户端都需要创建 Socket 对象
但是 服务器 的 socket 一般要显式的指定一个端口号.而 客户端 的 socket 一般不能显式指定(不显式指定,此时系统会自动分配一个随机的端口)


为什么啊???为什么服务器要指定端口号而客户端要系统自动分配客户端???

想象这样一个场景,现在你在你们学校的一食堂20号窗口卖东北盒饭(这是真香),然后我就在学校门口,发传单~当有的同学看到传单之后,就想来尝一尝!!!

 要想让同学能找到我,传单上必须印清楚我的地址和窗口.也就是一食堂20号窗口。对于服务器来说,也就需要把端口号给明确下来了~~~

有同学来我这里吃东北盒饭.点好餐,这几个同学就找了个旁边的位置就坐下了我这边把盒饭做好,就可以端过去了~~~这几个同学如果经常来我这吃饭,那么他们每次坐的位置,是否要求必须一样??不需要~~ 也不一定能做到~~可能第二次来的时候,这个位置被别人坐了,他们再找个空的位置就行了.

客户端的端口号是不需要确定的.交给系统进行分配即可,如果你手动指定确定的端口,就可能和别人的程序的端口号冲突


那这时候又有问题了,说煮啵煮啵为什么服务器这边手动指定端口,就不会出现冲突吗???

为什么客户端在意这个冲突,服务器不在意呢???

答:服务器是在程序猿手里的.一个服务器上都有哪些程序, 都使用哪些端口,程序猿都是可控的!!!   程序猿写代码的时候,就可以指定一个空闲的端口,给当前的服务器使用即可.但是客户端就不可控,客户端是在用户的电脑上,-方面,用户千千万~~ 每个用户电脑上装的程序都不一样,占用的端口也不一样另一方面,用户这边如果出现端口冲突了,他也不知道是端口冲突,他也不知道该咋解决,只会骂这个程序员,你这啥程序员,写的啥程序,全是bug.交给系统分配比较稳妥.系统能保证肯定分配一个空闲的端口。


requestPacket这个对象用来承载从网卡这边读到的数据.收到数据的时候,需要搞一个内存空间来保存这个数据DatagramPacket 内部不能自行分配内存空间因此就需要程序员手动把空间创建好,交给 DatagramPacket 进行处理~~

receive 方法的核心目的是:【阻塞当前线程,直到接收到一个 UDP 数据包

这个步骤是一个服务器程序,最核心的步骤!!!
咱们当前是 echo server 不涉及到这些流程,也不必考虑响应怎么计算,只要请求过来,就把请求当做响应UDP 无连接的~~ (UDP 自身不会保存数据要发给谁),就需要每次发送的时候,重新指定,数据要发到哪里去!!

 

send 方法的作用是:【将指定的 DatagramPacket 数据报文发送到目标地址】。


我再来提个问题 ,说煮啵煮啵上述写的代码中,为啥没写 close??socket 也是文件,不关闭不就出问题了,不就文件资源泄露了嘛~~

socket 是文件描述符表中的一个表项每次打开一个文件,就会占用一个位置,文件描述符, 是在 pcb 上的.(跟随进程的),这个 socket 在整个程序运行过程中都是需要使用的(不能提前关闭)当 socket 不需要使用的时候, 意味着程序就要结束了.进程结束,此时随之文件描述符表就会销毁了(PCB 都销毁了).随着销毁的过程,被系统自动回收了~~

啥时候才会出现泄露?

代码中频繁的打开文件,但是不关闭.在一个进程的运行过程中,不断积累打开的文件,逐渐消耗掉文件描述符表里的内容最终就消耗殆尽了.但是如果进程的生命周期很短,打开一下没多久就关闭了.谈不上泄露

在此代码中,用到了 3个 DatagramPacket 的构造方法

1.只指定字节数组缓冲区的.(服务器收请求的时候需要使用, 客户端收响应的时候也需要使用)

2.指定字节数组缓冲区, 同时指定一个 |netAddress 对象 (这个对象就同时包含了 IP 和 端口)(服务器返回响应给客户端)

3.指定字节数组缓冲区, 同时指定 IP +端口号

下面是程序运行时的顺序与思维导图,让我们更好的理解代码

1.服务器先启动.服务器启动之后,就会进入循环,执行到 receive 这里并阻塞 (此时还没有客户端过来呢) 

2.客户端开始启动,也会先进入 while 循环,执行 scanner.next,并且也在这里阻塞当用户在控制台输入字符串之后,next 就会返回,从而构造请求数据并发送出来~~
3.客户端发送出数据之后,服务器: 就会从 receive 中返回,进一步的执行解析请求为字符串,执行 process 操作,执行 send 操作
客户端: 继续往下执行,执行到 receive,等待服务器的响应
4.客户端收到从服务器返回的数据之后,就会从 receive 中返回,执行这里的打印操作,也就把响应给显示出来了.
5.服务器这边完成一次循环之后, 又执行到 receive 这里.客户端这边完成一次循环之后,又执行到 scanner.next 这里双双进入阻塞

六.TCP的简单的客户端/服务器


TCP 的 socket ap和 UDP 的 socket api 差异又很大~~

两个关键的类.
1.ServerSocket(给服务器使用的类, 使用这个类来绑定端口号)
2.Socket(既会给服务器用, 又会给客户端用)

这俩类都是用来表示 socket 文件的(抽象了网卡这样的硬件设备),TCP 是字节流的传输的基本单位,是 byte

再来回顾一下TCP 和 UDP 的特点对比吧 

1.TCP 是有连接的. UDP 无连接

连接: 通信双方是否会记录保存对端的信息
UDP 来说,每次发送数据都得手动在 send 方法中指定
目标的地址.(UDP 自身没有存储这个信息)
TCP 来说,则不需要,前提是需要先把连接给建立上.

2.TCP 是可靠传输.UDP 不可靠传输
3.TCP 是面向字节流. UDP 是面向数据报
4. TCP 和 UDP 都是全双工.

现在我们已经看过了UDP的服务器和客户端,下面我们直接先来看TCP的代码,与前面UDP不一样的方面或者你所存在的疑问我们在代码后面详细讲解

TCP的服务器代码:

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;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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("服务器启动!!!");
        //创建线程池
        ExecutorService service = Executors.newCachedThreadPool();
        while(true){
            //通过accept方法,把内核中已经建立好的链接拿到应用程序中
            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);
                    }
                }
            });

        }
    }

    //通过这个方法来处理当前的连接
    private void processConnection(Socket clientSocket) throws IOException {
        //进入方法,先打印一个日志,表示当前有客户端连接上了
        System.out.printf("[%s:%d] 客户端上线!!!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
                OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            while(true)
            {

                if(!scanner.hasNext()){
                    System.out.printf("[%s:%d] 客户端下线!!!\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //1.读取请求并解析
                String request = scanner.next();
                //2.根据请求,计算响应
                String response = process(request);
                //3.把响应写回客户端
                //  使用PrintWriter把 OutputStream包裹一下,来写入字符串
                PrintWriter printWriter = new PrintWriter(outputStream);
                //此处的printf不是打印到控制台了,而是写入到了对应的流对象中了,也就是clientSocket里面了
                //自然这个数据也就通过网络发送出去了
                printWriter.println(response);
                //刷新缓冲区
                printWriter.flush();

                //打印一下请求交互的内容
                System.out.printf("[%s:%d] req=%s resp=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }
    }

    private String process(String request) {
        return request;
    }

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

下面是客户端的代码:

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;


    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //需要在创建Socket的同时,和服务器“建立连接”,此时就的告诉Socket服务器在哪
        //建立的细节,内核自动负责的
        socket = new Socket(serverIp, serverPort);
    }
    public void start(){
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream =socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            PrintWriter writer = new PrintWriter(outputStream);
            Scanner scannerNetwork = new Scanner(inputStream);
            while(true){
                //1.从控制台读取用户输入的数据
                System.out.printf("->");
                String request = scanner.next();
                //2.把字符串作为请求,发给服务器
                //  这里使用Printfln,是为了让请求后面带上换行
                //  也就是和服务器读取请求,scanner.next 呼应
                writer.println(request);
                writer.flush();
                //3.读取服务器返回的响应
                String response = scannerNetwork.next();
                //在界面上显示内容
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

下面讲解TCP这边的重点:

在代码中,这句实在是太显眼了,这try是什么,这里面又是什么意思???

try 代码块在执行结束后自动关闭,避免忘记手动释放导致内存泄漏。InputStream 和 OutputStream 就是 字节流!!!就可以借助这俩对象, 完成数据的"发送"和“接收
通过 InputStream 进行 read 操作,就是“接收"
通过 OutputStream 进行 write 操作,就是“发送”

这里遇到了next(),这又是啥意思???

next的规则是,读到“空白符”就返回,这里说的空白符是一类特殊的字符,换行, 回车符,空格, 制表符,翻页符,垂直制表符....

后续客户端发起的请求,会以空白符作为结束标记(此处就约定使用\n),TCP 是字节流通信方式,每次传输多少个字节,每次读取多少个字节
都是非常灵活的,但是灵活往往是坏事~~~代码就不知道该咋办了~往往会手动约定出,从哪里到哪里是一个完整的数据报每次循环一次,就处理一个数据报即可.上述这里就是约定了使用 \n作为数据报的结束标记. 就正好可以搭配scanner.next 来完成请求的读取过程,

到这里,让我们回忆一下UDP的客户端,与TCP的客户端对比一下

无论是TCP还是UDP客户端代码里出现 String request = scanner.next();,它的真实意义是:从用户控制台输入获取数据(比如键盘输入),并不是直接操作网络协议的数据收发。

关键区别

  • TCP客户端后续会将这个字符串通过 Socket的OutputStream以字节流形式发送到服务端
  • UDP客户端则会将字符串包装成DatagramPacket(数据报),通过DatagramSocket独立发送。
  • 真实的协议差异体现在发送逻辑(流还是数据报)

 不知道大家发现了没有,TCP的代码与UDP的代码还有几处打不一样的地方

1,

如果没有这几行代码,服务器程序就会出现文件资源泄露,Socket ClientSocket 这个对象要进行close,前面写过的 DatagramSocket, ServerSocket 都没写 close,但是我们说这个东西都没关系但是 clientSocket 如果不关闭,就会真的泄露了!!!
DatagramSocket 和 ServerSocket, 都是在程序中, 只有这么一个对象,申明周期,都是贯穿整个程序的ClientSocket 则是在循环中,每次有一个新的客户端来建立连接,都会创建出新的 clientSocket

每次执行这个,都会创建新的 clientsockei并且这个 socket 最多使用到 该客户端退出 (断开连接)此时,如果有很多客户端都来建立连接~此时,就意味着每个连接都会创建 clientSocket,当连接断开clientSocket 就失去作用了.但是如果没有手动 close,此时这个 socket 对象就会占据着文件描述符表的位置

在上面不是说try可以自动关闭吗???

这里的关闭,只是关闭了 clientSocket 上自带的流对象,并没有关闭 socket 本身

 如果启动多个客户端,多个客户端可以同时和服务器建立连接吗???

因为上面的代码已经是改好的了,当然是可以的了,下面我们看一下之前的服务器代码

如果是这么写的呢,当前启动两个客户端,同时连接服务器,其中一个客户端(先启动的客户端)一切正常,另一个客户端(后启动的客户端)则没法和服务器进行任何交互.(服务器不会提示“建立连接”,也不会针对 请求 做出任何响应),现在看到的现象,就是当前代码的一个很明显的问题!!(bug)
这个 bug 和当前的代码结构是密切相关的!!

第一个客户端过来之后,accept 就返回了,得到一个 clientSocket.进入了 processConnection.又进入了一个 while 循环,这个循环中,就需要反复处理客户端发来的请求数据,如果客户端这会没发请求,服务器的代码就会阻塞在scanner.hasNext 这里,第一个客户端就会使服务器处于processConnection内部

进一步的也就使当前的第一层循环,无法执行到accpt,直到第一个客户端退出,processConnection才能结束,才能执行到第二次accept

确实如刚才推理的现象一样,第一个客户端结束的时候,就从 processConnection 返回了就可以执行到第二次 accept 了,也就可以处理第二个客户端了~~
很明显,如果启动第三个客户端,第三个客户端也会僵硬住,又会需要第二个客户端结束才能活过来如何解决上述问题?让一个服务器可以同时接待多个客户端呢??关键就是,在处理第一个客户端的请求的过程中,要让代码能够快速的第二次执行到 accept ~~~

当然是多线程

这样新的线程负责在 processConnection 里面来循环处理客户端的请求了.

但是问题又来了,此时这个服务器, 每个客户端都要创建一个线程.如果有很多客户端.频繁的来进行建立连接/断开连接,这个时候就会导致服务器频繁的 创建/销毁 线程.(开销就很大了)还可以使用线程池,来进一步的优化~~

线程池的方式是可以降低"频繁创建销毁线程的开销”

OK了,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值