网络编程套接字

先赞后看,养成习惯!!! ^ _ ^ ❤️ ❤️ ❤️

码字不易,大家的支持就是我坚持下去的动力,点赞后不要忘记关注我哦

📘 本系列文章为本人在学习路上遇到的问题和解决方法,在这里撰写成文是为了巩固知识和帮助其他友友。

个人主页 🔍: 加瓦糕手

专栏链接 📁: 问题分析简介

如有错误,请您指正批评 ^ _ ^

1. 网络编程基础

为什么需要网络编程?-丰富的网络资源

用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。

与本地打开视频文件类似,只是视频文件这个资源的来源是网络。

相比本地资源来说,网络提供了更为丰富的网络资源:

所谓的网络资源,其实就是在网络中可以获取的各种数据资源。

而所有的网络资源,都是通过网络编程来进行数据传输的。

1.1 什么是网络编程?

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

当然,我们只要满足进程不同就行;即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。

但是,我们以定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:

进程A:编程来获取网络资源。

进程B:变成来提供网络资源。

1.2 网络中的基本概念

发送端和接收端

再一次网络传输时:

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

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

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

**注意:**发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

请求和响应:

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

第一次:请求数据的发送。

第二次:响应数据的发送。

客户端和服务端

**服务端:**在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。

**客户端:**获取服务的一方进程,称为客户端。

对于服务来说,一般是提供:

客户端获取服务资源:

客户端保存资源在服务端:

常见的客户端服务端模型

最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:

  1. 客户端先发送请求到服务端

  2. 服务端根据请求数据,执行相应的业务处理

  3. 服务端返回响应:发送业务处理结果

  4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

2. Socket套接字

2.1 概念

Socket套接字,是系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。(相当于提供一个Socket接口,用户可以通过Socke套接字来实现send、receive数据)。

2.2 分类

Socket套接字主要针对传输层协议划分为如下三类:

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

TCP即传输控制协议,传输层协议。

以下为TCP的特点:

有连接、可靠传输、面向字节流、有接收缓冲区、也有发送缓冲区、大小不限。

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

UDP即用户数据报协议,传输层协议。

以下为UDP的特点:

无连接、不可靠传输、面向数据报、有接收缓冲区、无发送缓冲区、大小受限:一次最多传输64K。

对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。

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

2.3 Java数据报套接字通信模型

对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。

java中使用UDP协议通信,主要基于DatagramSocket 类来创建数据报套接字,并使用DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:

以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:

2.4 Java流套接字通信模型

3. UDP数据报套接字编程

API介绍

DatagramSocket

DatagramSocket是UDP Socket,用于发送和接收UDP数据报。

DatagramSocket构造方法:

|--------------------------|----------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)。 |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |

DatagramSocket方法:

|--------------------------------|---------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |

DatagramPacket

DatagramPacket 是UDP Socket发送和接收的数据报。

DatagramPacket 构造方法:

|----------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte[] buf, int length) | 构造⼀个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
| DatagramPacket(byte[] buf, int offset,int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP 和端口号。 |

DatagramPacket 方法:

|--------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址。 |
| int getPort() | 从接收的数据报中,获取发送端主机的端⼝号;或从发送的数据报中,获取接收端主机端⼝号。 |
| byte[] getData() | 获取数据报中的数据。 |

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用InetSocketAddress 来创建。

InetSocketAddress

InetSocketAddress(SocketAddress的子类)构造方法

|----------------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetSocketAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号 |

3.1 代码示例

UDP Echo Server

java 复制代码
package network;

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

public class EchoServer {
    private DatagramSocket socket;
    public EchoServer(int port) throws SocketException {
        socket=new DatagramSocket(port);
    }
    //启动服务器,完成主要的业务逻辑
    public void start() throws IOException {
        System.out.println("服务端启动");
        while (true){
            // 1. 读取请求并解析
            //  1) 创建一个空白的 DatagramPacket 对象
            DatagramPacket reqPacket=new DatagramPacket(new byte[4096],4096);
            //  2) 通过 receive 读取网卡的数据. 如果网卡没有收到数据, 就会阻塞等待.
            socket.receive(reqPacket);
            //  3) 把 DatagramPacket 中的数据解析成字符串. 只需要从 DatagramPacket 取到有效的数据即可.
            String request=new String(reqPacket.getData(),0,reqPacket.getLength());
            //2.计算响应
            String response= process(request);
            //3.写回到客户端
            DatagramPacket resPacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
                    reqPacket.getSocketAddress());
            socket.send(resPacket);
            // 4. 打印日志.
            System.out.printf("[%s:%d] req: %s, resp: %s\n",
                    reqPacket.getAddress(), reqPacket.getPort(), request, response);
        }
    }
    //回显服务器
    public String process(String request){
        return request;
    }

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

UDP Echo Client

java 复制代码
package network;

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

public class EchoClient {
    private DatagramSocket socket=null;
    private String serverIp;
    private int serverPort;
    public EchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        Scanner scanner=new Scanner(System.in);
        System.out.println("客户端启动!");
        while (true){
            System.out.print(">");
            String request=scanner.next();
            DatagramPacket reqPacket=new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
            socket.send(reqPacket);
            //3.读取服务器的响应
            DatagramPacket respPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(respPacket);
            String response=new String(respPacket.getData(),0,respPacket.getLength());
            // 4. 把响应显示到控制台.
            System.out.println(response);
        }
    }

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

UDP Dict Server

编写一个英译汉的服务器。只需要重写process。

java 复制代码
package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class DictServer extends EchoServer{
    private Map<String,String> dict=new HashMap<>();
    public DictServer(int port) throws SocketException {
        super(port);
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("pig", "小猪");
        dict.put("bird", "小鸟");
        dict.put("sheep", "小羊");
        dict.put("cow", "小牛");
        dict.put("chicken", "小鸡");
        dict.put("rabbit", "小兔子");
        dict.put("fish", "小鱼");
        dict.put("wolf", "狼");
        dict.put("monkey", "猴子");
        dict.put("chicken", "小鸡");
        dict.put("fish", "小鱼");
        dict.put("tiger", "老虎");
        dict.put("lion", "狮子");
        dict.put("wolf", "狼");
        dict.put("monkey", "猴子");
    }
    @Override
    public String process(String request){
        return dict.getOrDefault(request,"该单词没有查到");
    }
    public static void main(String[] args) throws IOException {
        DictServer server=new DictServer(9090);
        server.start();
    }
}

4. TCP流套接字编程

API介绍

ServerSocket

ServerSocket 是创建TCP服务端Socket的API。

ServerSocket 构造方法:

|------------------------|---------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务端套接字Socket,并绑定到指定端口 |

ServerSocket 方法:

|-----------------|-------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客户端连接后,返回⼀个服务端Socket对象,并基于该 Socket建立与客户端的连接,否则阻塞等待。 |
| void close() | 关闭此套接字。 |

Socket

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服 务端Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

Socket 构造方法:

|------------------------------|-------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host,int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。 |

Socket 方法:

|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |

4.1 代码示例

TCP Echo Server

java 复制代码
package network.tcp;

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;

public class EchoServer {
    private ServerSocket serverSocket;
    public EchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        Socket socket=serverSocket.accept();
        processConnection(socket);
    }
    private void processConnection(Socket socket){
        System.out.printf("[%s:%d] 服务器上线!\n", socket.getInetAddress().toString(), socket.getPort());
        try (InputStream inputStream=socket.getInputStream();
             OutputStream outputStream=socket.getOutputStream()){
            // 实现通信的代码.
            // 一个客户端可能会和服务器有多轮的请求响应交互.
            Scanner scanner=new Scanner(inputStream);
            PrintWriter writer=new PrintWriter(outputStream);
            while (true){
                if (!scanner.hasNext()){
                    // 针对客户端下线逻辑的处理. 如果客户端断开连接了 (比如客户端结束了)
                    // 此时 hasNext 就会返回 false
                    // 如果是使用 read 方法, 就会出现返回 -1 的情况. 也可以用来判定客户端断开连接.
                    System.out.printf("[%s:%d] client offline!\n", socket.getInetAddress().toString(), socket.getPort());
                    break;
                }
                String request=scanner.next();
                String response=process(request);
                writer.println(response);
                writer.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n", socket.getInetAddress().toString(), socket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private String process(String request) {
        return request;
    }

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

TCP Echo Client

java 复制代码
package network.tcp;

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;

public class EchoClient {
    private Socket socket;
    public EchoClient(String serverIp,int serverPort) throws IOException {
        // 在 new 这个对象的时候就涉及到 "建立连接操作"
        // 由于连接建立好了之后, 服务器的信息就在操作系统中被 TCP 协议记录了. 我们在应用程序层面上就不需要保存 IP 和 端口.
        socket = new Socket(serverIp, serverPort);
    }
    public void start(){
        System.out.println("客户端启动");
        Scanner scanner=new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scannerNet=new Scanner(inputStream);
            PrintWriter writer=new PrintWriter(outputStream);
            while (true){
                // 1. 从控制台读取用户的输入.
                System.out.print("> ");
                String request=scanner.next();
                writer.println(request);
                writer.flush();
                if (!scannerNet.hasNext()) {
                    System.out.println("server disconnect");
                    break;
                }
                String response=scannerNet.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

5. 服务器引入多线程

如果只是单个线程,无法同时响应多个客户端。

此处给每个客户端都分配一个线程。

java 复制代码
  public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket socket=serverSocket.accept();
            Thread t=new Thread(()->{
               processConnection(socket); 
            });
            t.start();
        }
    }

5.1 服务器引入线程池

java 复制代码
// 启动服务器 
public void start() throws IOException {
    System.out.println("服务器启动!");
    ExecutorService service = Executors.newCachedThreadPool();
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 使⽤线程池, 来解决上述问题 
        service.submit(new Runnable() {
            @Override
            public void run() {
                processConnection(clientSocket);
            }
        });
    }
}

6. 长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。

长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

对比以上长短连接,两者区别如下:

建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要 第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时 的,长连接效率更高。

主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送 请求,也可以是服务端主动发。

两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于 客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

相关推荐
阿巴~阿巴~1 小时前
解锁HTTP方法奥秘:GET与POST的深度探索与实战演示
服务器·网络·网络协议·http·get·post·请求方法
inquisiter1 小时前
cove-salus-tellus测试程序时序逻辑
linux·服务器·网络·riscv
向葭奔赴♡1 小时前
Android AlertDialog实战:5种常用对话框实现
android·java·开发语言·贪心算法·gitee
坐不住的爱码1 小时前
静态资源映射-spring整合
java·spring·状态模式
大佐不会说日语~1 小时前
基于Spring AI Alibaba的AI聊天系统中,流式输出暂停时出现重复插入问题的分析与解决
java·人工智能·spring
0和1的舞者1 小时前
API交互:前后端分离开发实战指南
java·spring·tomcat·web3·maven·springmvc·springweb
一 乐1 小时前
宠物店管理|基于Java+vue的宠物猫店管理管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
天天摸鱼的小学生1 小时前
【Java泛型一遍过】
java·开发语言·windows
代码不行的搬运工1 小时前
使用多代理间 AS 诊断系统检测和恢复前缀劫持(2010)
网络·bgp安全