【网络基础二】网络编程基础

目录

一 前言

二 Socket套接字

三 UDP数据报套接字编程

四 TCP流套接字编程


一 前言

1.为什么需要网络编程?

  在生活中,我们可以通过网络获取丰富多彩的网络资源。比如,我们在B站上看到的各种视频,实质都是通过网络获取到的资源。而所有的网络资源,都是通过网络编程来进行数据传输的

2.网络编程的基本概念

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

②基本概念:

  • 发送端和接收端

发送端:数据的发送方进程,即源主机。

接收端:数据的接收方进程,即目的主机。

收发端:发送端和接收端的两端。

  • 请求和响应

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

第一次:请求数据发送

第二层:响应数据发送

  • 客户端和服务端

服务端:提供服务的一方进程。

客户端:获取服务的一方进程。

二 Socket套接字

①概念:

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

②分类:

针对传输层协议分为三类:

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

    流式数据的特征就是在IO流没有关闭的情况下,可以多次发送,也可以分开多次接收。

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

    对于数据报来说,传输数据是一块一块的,假如发送一块100个字节的数据,那么发送和接收必须都是一次100字节,而不能分100次发送,每次接收一字节。

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

③注意事项:

1、客户端与服务端在开发时是基于一个主机的两个进程,而在真实场景中,一般都是不同主机。

2、目的IP和目的端口号标识了数据传输时要发送到的目的主机和进程。

3、Socket编程是基于传输层的TCP/UDP协议使用的流套接字和数据报套接字,但应用层协议也要考虑。

三 UDP数据报套接字编程

UDP:User Datagrm Protocol(用户数据报协议)

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

1.DatagramSocket  API

用于发送和接收UDP数据报,代表一个socket对象。

构造方法:

方法:

2.DatagramPacket  API

代表一个UDP数据报,构造对象,在指定字节数组作为缓存区。

构造方法:

方法:

3.InetSocketAddress  API

构造方法:

示例一:一发一收(无响应)

以下为一个客户端一次数据发送和服务端多次数据接收(只有客户端请求,没有服务端响应)

UDP服务端

public class UdpServer {
    //服务器socket绑定固定的端口
    private static final int PORT = 8888;

    public static void main(String[] args) throws IOException {
        //1.创建服务端DatagramSocket,指定端口,可以发送并接收UDP数据报
        DatagramSocket socket = new DatagramSocket(PORT);
        //只要有客户端的数据报就要接收
        while(true){
            //2.创建数据报,用于接收客户端发送的数据
            byte[] bytes = new byte[1024];
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
            System.out.println("-------------------");
            System.out.println("等待接收UDP数据报...");

            //3.等待客户端发送的UDP数据报,在接收到数据报之前一直阻塞,接收到数据报以后,DatagramPacket对象包含数据(bytes),客户端ip,端口号
            socket.receive(packet);
            System.out.printf("客户端IP:%s%n", packet.getAddress().getHostAddress());
            System.out.printf("客户端端口号:%s%n", packet.getPort());
            System.out.printf("客户端发送的原生数据:%s%n", Arrays.toString(packet.getData()));
            System.out.printf("客户端发送的文本数据为:%s%n", new String(packet.getData()));
        }

    }
}

UDP客户端

public class UdpClient {
    //服务端socket地址:域名或IP,端口号
    private static final SocketAddress ADDRESS = new InetSocketAddress("localhost",8888);

    public static void main(String[] args) throws IOException {
        //创建客户端DatagramSocket,开启随意端口,可以发送及接收数据报
        DatagramSocket socket = new DatagramSocket();

        //准备要发送的数据
        byte[] bytes = "hello world!".getBytes();

        //组装要发送的UDP数据报,包含数据,服务器IP,端口号
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length, ADDRESS);

        //发送UDP数据报
        socket.send(packet);
    }
}

示例二:请求响应

对示例一进行改造:构造一个展示服务端本地某个目录(BASE_PATH)的下一子级文件列表的服务。

1.客户端先接收键盘输入,表示要展示的相对路径

2.发送请求:将该相对路径作为数据报发送到服务端

3.服务端接收并处理请求:根据请求数据,作为本地目录的路径,列出下一子级文件及文件夹

4.服务端返回响应:遍历子文件和文件夹,每个文件每一行作为响应的数据报返回给客户端

5.客户端接收响应:打印输出响应内容

为了解决空字符或长度不足数据丢失的问题,客户端服务端约定好统一的协议:这里简单的设计为 ASCII结束字符 \3 表示报文结束。
UDP服务端:
public class UdpServer {
    //服务器socket绑定固定的端口
    private static final int PORT = 8888;
    //本地文件目录要展示的根目录
    private static final String BASE_PATH = "D:/TEST";

    public static void main(String[] args) throws IOException {
        //1.创建服务端DatagramSocket,指定端口,可以发送并接收UDP数据报
        DatagramSocket socket = new DatagramSocket(PORT);
        //不停接收客户端udp数据报
        while(true){
            //2.创建数据报,用于接收客户端发送的数据
            byte[] requestData = new byte[1024];
            DatagramPacket requestpacket = new DatagramPacket(requestData, requestData.length);
            System.out.println("-------------------");
            System.out.println("等待接收UDP数据报...");

            //3.等待客户端发送的UDP数据报,在接收到数据报之前一直阻塞,接收到数据报以后,DatagramPacket对象包含数据(bytes),客户端ip,端口号
            socket.receive(requestpacket);
            System.out.printf("客户端IP:%s%n", requestpacket.getAddress().getHostAddress());
            System.out.printf("客户端端口号:%s%n", requestpacket.getPort());
            //4.接收到的数据作为请求,根据请求数据执行业务并返回响应
            for (int i = 0; i < requestData.length; i++) {
                byte b = requestData[i];
                if(b == '\3'){
                    //4.1读取请求数据,读取到约定好的结束符(\3),取结束符之前的内容
                    String request = new String(requestData, 0, i);

                    //4.2根据请求处理业务:本地目录根路径+请求路径,作为要展示的目录列出下一自己文件
                    //请求的文件列表目录
                    System.out.printf("客户端请求的文件列表路径:%s%n", BASE_PATH + request);
                    File dir = new File(BASE_PATH + request);
                    //获取下一子级文件,文件夹
                    File[] children = dir.listFiles();

                    //4.3构造要返回的响应内容:每个文件及目录名称为一行
                    StringBuilder response = new StringBuilder();
                    if(children != null){
                        for(File child : children){
                            response.append(child.getName()+"\n");
                        }
                    }
                    //响应也要约定结束符
                    response.append("\3");
                    byte[] responseData = response.toString().getBytes(StandardCharsets.UTF_8);

                    //4.4构造返回响应的数据报DatagramPacket,注意接受的客户端数据包含ip和端口号
                    DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length, requestpacket.getSocketAddress());

                    //4.5发送返回响应的数据报
                    socket.send(responsePacket);
                    break;
                }
            }
        }

    }
}

UDP客户端:

public class UdpClient {
    //1.服务端socket地址:域名或IP,端口号
    private static final SocketAddress ADDRESS = new InetSocketAddress("localhost",8888);

    public static void main(String[] args) throws IOException {
        //2.,开启随意端口,可以发送及接收数据报
        DatagramSocket socket = new DatagramSocket();

        //3.1准备要发送的数据:这里调整为键盘输入作为发送的内容
        Scanner sc = new Scanner(System.in);
        while(true){
            System.out.println("-------------");
            System.out.println("请输入要展示的目录:");
            //3.2每输入的新行就作为要发送的数据报
            String request = sc.nextLine()+"\3";
            byte[] requestData = request.getBytes(StandardCharsets.UTF_8);
            //3.3组装要发送的UDP数据报:包含数据,服务器IP,端口号
            DatagramPacket requestpacket = new DatagramPacket(requestData, requestData.length, ADDRESS);

            //4.发送UDP数据报
            socket.send(requestpacket);

            //5.接收服务端响应的数据报,并根据响应内容决定下一个步骤
            //5.1创建数据报,用于接收服务端发送的响应
            byte[] responseData = new byte[1024];
            DatagramPacket responsepacket = new DatagramPacket(responseData, responseData.length);

            //5.2接收响应数据报
            socket.receive(responsepacket);
            System.out.println("该目录下的文件列表为:");
            //byte下次解析的起始位置
            int next = 0;
            for (int i = 0; i < responseData.length; i++) {
                byte b = responseData[i];
                if(b == '\3')
                    break;
                if(b == '\n'){
                    String filename = new String(responseData, next, i-next);
                    System.out.println(filename);
                    next = i+1;
                }
            }

        }
    }
}

示例三:回显服务器(echo server)

说明:回显服务器省略了"根据请求计算响应”,请求是什么,响应就是什么。

UDP服务器:

//UDP版本的回显服务器
public class UdpEchoServer {
    //1.建立一个socket对象并绑定具体端口号
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            //2.读取客户发过来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //3.根据请求计算响应,由于此处是回显服务器,请求和响应相同
            String response = process(request);
            //4.把响应写回到客户端,send的参数是DatagramPacket,需要用响应数据构造好packet对象
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);

            //5.打印一下,当前这次请求响应的中间结果
            System.out.printf("[%s:%d]req: %s; resp:%s\n", responsePacket.getAddress().toString(), requestPacket.getPort(), request, response);
        }
    }

    //process:根据请求计算响应
    public String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        //端口号可以随便指定:1024-65535
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

UDP客户端:

//UDP版本的回显客户端
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP = null;
    private int serverPort = 0;

    /*一次通信需要两个ip,两个端口
    客户端的ip是127.0.0.1已知,port是系统自动分配的
    服务端的ip和端口也要告诉客户端
    */
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException{
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException{
        System.out.println("客户端启动!");
        Scanner sc = new Scanner(System.in);
        while(true){
            //1.从控制台读取要发送的数据
            System.out.print("> ");
            String request = sc.next();
            if(request.equals("exit")){
                System.out.println("goodbye");
                break;
            }
            //2.构造UDP请求并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes(
            ).length, InetAddress.getByName(serverIP), serverPort);
            socket.send(requestPacket);
            //3.读取服务器的UDP响应并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            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 client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

四 TCP流套接字编程

TCP:Transmission Control  Protocol(传输控制协议)

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

1.ServerSocket API

专门给服务器用的,accept用来接收一个连接

构造方法:

方法:

2..Socket API

服务器和客户端都会用到:客户端使用Socket和服务器建立连接,并且后续进行传输;服务器使用Socket和客户端进行交互。

构造方法:

方法:

Socket提供了getInputStream和getOutputStream获取输入输出流,进一步通过这些流对象来完成数据传输。

TCP中的长短连接

短连接:每次接收到数据并返回响应后,都关闭连接。(只能一次收发数据)

长连接:不关闭连接,一直保持连接状态,双方不停地收发数据。(多次收发数据)

区别:

  • 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;长连接可以是客户端主动发送请求,也可以是服务端主动发送。
  • 使用场景不同:短连接适合客户端请求频率不高的场景,比如浏览网页;长连接适用于客户端与服务端通信频繁的场景,如游戏。
  • 建立连接、关闭连接耗时:短连接每次请求、响应都需要建立连接、关闭连接;而长连接只需要建立一次。相比之下,长连接效率更高。

示例一:一发一收(短连接)

TCP服务端:

public class TcpServer {
    //服务器socket要绑定固定端口
    private static final int PORT = 8888;
    public static void main(String[] args) throws IOException {
        //1.创建一个服务端ServerSocket,用于收发TCP报文
        ServerSocket server = new ServerSocket(PORT);
        while(true){
            System.out.println("-------------------");
            System.out.println("等待客户端建立TCP连接...");
            //2.等待客户端连接,注意该方法为阻塞方法
            Socket client = server.accept();
            System.out.printf("客户端IP:%s%n",client.getInetAddress().getHostAddress());
            System.out.printf("客户端端口号:%s%n",client.getPort());
            //3.接收客户端数据,需要从客户端Socket中的输入流获取
            System.out.println("接收到的客户端请求:");
            InputStream is = client.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            String line;
            while((line = br.readLine()) != null){
                System.out.println(line);
            }
            //4.双方关闭连接:服务端关闭的是客户端socket连接
            client.close();
        }
    }
}

TCP客户端:

public class TcpClient {
    //服务端IP或域名
    private static final String SERVER_HOST = "localhost";

    //服务端Socket进程的端口号
    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) throws IOException {
        //1.创建一个客户端流套接字,并与对应的IP主机对应端口的进程建立连接
        Socket client = new Socket(SERVER_HOST, SERVER_PORT);
        //2.发送TCP数据,通过socket中的输出流进行发送
        OutputStream os = client.getOutputStream();
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(os,"UTF-8"));
        //3.1发送数据
        pw.println("hello world!");
        //3.2有缓冲区的IO操作,真正传输数据,需要刷新缓冲区
        pw.flush();
        //3.3双方关闭连接:客户端关闭Socket连接
        client.close();

    }
}

示例二:请求响应(短连接)

示例一只是客户端发出请求和服务端接收请求,并没有包含服务端的返回响应,以下是对请求和响应做出的改进:

构造一个展示服务端本地某个目录( BASE_PATH )的下一级子文件列表的服务

TCP服务端:

public class TcpServer {
    //服务器socket要绑定固定端口
    private static final int PORT = 8888;
    private static final String BASE_PATH = "D:/TEST";

    public static void main(String[] args) throws IOException {
        //1.创建一个服务端ServerSocket,用于收发TCP报文
        ServerSocket server = new ServerSocket(PORT);
        while(true){
            System.out.println("-------------------");
            System.out.println("等待客户端建立TCP连接...");
            //2.等待客户端连接,注意该方法为阻塞方法
            Socket client = server.accept();
            System.out.printf("客户端IP:%s%n",client.getInetAddress().getHostAddress());
            System.out.printf("客户端端口号:%s%n",client.getPort());
            //3.接收客户端数据,需要从客户端Socket中的输入流获取
            InputStream is = client.getInputStream();

            BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8"));

            String request = br.readLine();

            System.out.printf("客户端请求的文件列表路径为:%s%n",BASE_PATH+request);
            File dir = new File(BASE_PATH+request);
            File[] children = dir.listFiles();
            OutputStream os = client.getOutputStream();
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(os,"UTF-8"));
            if(children != null){
                for (File child : children){
                    pw.println(child.getName());
                }
            }
            pw.flush();
            client.close();
        }
    }

TCP客户端:

public class TcpClient {
    //服务端IP或域名
    private static final String SERVER_HOST = "localhost";

    //服务端Socket进程的端口号
    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        while(true){
            System.out.println("------------");
            System.out.println("请输入要展示的目录:");
            String request = sc.nextLine();
            Socket socket = new Socket(SERVER_HOST,SERVER_PORT);
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(os,"UTF-8"));
            pw.println(request);
            pw.flush();
            System.out.println("接收到服务端响应");
            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8"));
            String line;
            while((line = br.readLine()) != null){
                System.out.println(line);
            }
            socket.close();
        }

    }
}

示例四:

TCP服务端:

TCP服务器如果不使用多线程可能会无法处理多个客户端。因为accept和read也会阻塞,如果其中一处阻塞另一处也就无法处理数据,使用多线程就是为了规避阻塞问题。而UDP只有receive一处阻塞,就不存在上述问题了。

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("启动服务器!");
        //此处CashedThreadPool
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while(true){
            //使用clientSocket和具体的客户端交流
            Socket clientSocket = serverSocket.accept();
            //使用线程池
            threadPool.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

    //使用这个方法来处理一个对应到客户端的连接,可能会涉及多次交互
    private void processConnection(Socket clientSocket) {
        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 sc = new Scanner(System.in);
                if(!sc.hasNext()){
                    //没有下一个数据,说明客户关闭了连接
                    System.out.printf("[%s:%d]客户端下线!\n",clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                String request = sc.next();
                //2.根据请求构造响应
                String response = process(request);
                //3.返回响应结果
                //OutputStream没有write String功能,可以把String里的字节数组拿出来进行写入或用字符流转换
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //flush刷新缓冲区,保证当前写入的数据确实发送出去了
                printWriter.flush();
                System.out.printf("[%s:%d]req: %s; resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request,response);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            //把close放到finally里面,保证一定能执行
            try{
                clientSocket.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

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

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

TCP客户端:


public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException{
        socket = new Socket(serverIp, serverPort);
    }

    public void start(){
        System.out.println("客户端启动!");
        Scanner sc = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){
            while(true){
                //1.先从键盘上读取用户输入的内容
                System.out.print("> ");
                String request = sc.next();
                if(request.equals("exit")){
                    System.out.println("goodbye");
                    break;
                }
                //2.把读到的内容构造成请求,发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();
                //3.读取服务器响应
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                //4.把响应内容显示到界面上
                System.out.println(response);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

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

五 总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值