「回忆2022,展望2023,我正在参加2022年终总结征文大赛活动」

前语

回忆2022,展望2023,博主给各位技能er带来了网络编程的万字总结!!!附有博主本人的理解以及代码(附有博主个人gitee库房地址)。

一、概念

Socket套接字,是由体系供给用于网络通讯的技能,是根据TCP/IP协议的网络通讯的根本操作单元。根据Socket套接字的网络程序开发便是网络编程。

Socket是站在应用层,做网络编程很重要的一个概念

传输层、网络层、数据链路层、物理层 都是经过OS+硬件来供给服务的,而应用层要享用OS供给的网络服务,需求经过OS供给的服务窗口(Socket)来享用服务。

拓展

OS原生的供给的体系调用(Linux上的网络编程):

int fd = socket();
setsocketopt(fd,TCP or UDP)

二、分类(三类)

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

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

TCP,即Transmission Control Protocol(传输控制协议),传输层协议。

以下为TCP的特点:

  • 有衔接
  • 可靠传输
  • 面向字节省
  • 有接纳缓冲区,也有发送缓冲区
  • 巨细不限

关于字节省来说,能够简略的理解为,传输数据是根据IO流,流式数据的特征便是在IO流没有封闭的情况下,是无边界的数据,能够屡次发送,也能够分隔屡次接纳。

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

UDP,即User Datagram Protocol(用户数据报协议),传输层协议。

以下为UDP的特点:

  • 无衔接
  • 不可靠传输
  • 面向数据报
  • 有接纳缓冲区,无发送缓冲区
  • 巨细受限:一次最多传输64k

关于数据报来说,能够简略的理解为,传输数据是一块一块的,发送一块数据假如100个字节,有必要一次发送,接纳也有必要一次接纳100个字节,而不能分100次,每次接纳1个字节。

2.3 原始套接字

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

三、UDP数据报套接字编程

关于UDP协议来说,具有无衔接,面向数据报的特征,即每次都是没有树立衔接,而且一次发送悉数数据报,一次接纳悉数的数据报。

3.1 Java数据报套接字通讯模型

java中运用UDP协议通讯,主要根据 DatagramSocket 类来创立数据报套接字,并运用 DatagramPacket 作为发送或接纳的UDP数据报。关于一次发送及接纳UDP数据报的流程如下:

Socket套接字(网络编程万字总结-附代码)

3.2 DatagramSocket API

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

3.2.1 DatagramSocket 结构办法:

Socket套接字(网络编程万字总结-附代码)
留意

  1. UDP服务器(Server):选用一个固定端口,便利客户端(Client)进行通讯; 运用 DatagramSocket(int port) ,就能够绑定到本机指定的端口,此办法可能有过错风险,提示该端口现已被其他进程占用。

  2. UDP客户端(Client):不需求选用固定端口(也能够用固定端口),选用随机端口; 运用 DatagramSocket() ,绑定到本机恣意一个随机端口

3.2.2 DatagramSocket 一般办法(属于DatagramSocket类):

Socket套接字(网络编程万字总结-附代码)
留意

  1. 一旦通讯两边逻辑含义上有了通讯线路,两边方位就平等了(谁都能够作为发送方和接纳方)

  2. 发送方调用的便是 send() 办法,接纳方调用的便是 receive() 办法

  3. 通讯完毕后,两边都应该调用 close() 办法进行资源收回

3.3 DatagramPacket API

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

这个类便是界说的报文包:通讯过程中的数据抽象

能够理解为:发送/承受的一个信封(五元组+函件)

3.3.1 DatagramPacket 结构办法:

Socket套接字(网络编程万字总结-附代码)
留意

  1. 作为接纳方:只需求供给寄存承受数据的方位(byte[] buf + int length)
  2. 作为发送方:需求有要发送的数据(byte[] buf +int offset +int length),要发送给谁(远端ip+远端port)
    Socket套接字(网络编程万字总结-附代码)

3.3.2 DatagramPacket 一般办法:

Socket套接字(网络编程万字总结-附代码)
留意

  1. 一般给服务器运用的是 getAddress() 办法和 getPort() 办法,用来获取客户端的ip地址和端口号port
  2. 一般给接纳者(能够是服务器也但是客户端)运用的是 getData() ,用来拿到“信”(对方进程发送的应用层数据)

3.4 InetSocketAddress API

InetSocketAddress ( SocketAddress 的子类 )结构办法:

Socket套接字(网络编程万字总结-附代码)

3.5 代码示例(有恳求和相应)

以下仅展示部分代码,完好代码能够看博主的gitee库房:

网络开发代码

UDP客户端

public class UserInputLoopClient {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        // 1. 创立 UDP socket
        Log.println("预备创立 UDP socket");
        DatagramSocket socket = new DatagramSocket();
        Log.println("UDP socket 创立完毕");
        System.out.print("请输入英文单词: ");
        while (scanner.hasNextLine()) {
            // 2. 发送恳求
            String engWord = scanner.nextLine();
            Log.println("英文单词是: " + engWord);
            String request = engWord;
            byte[] bytes = request.getBytes("UTF-8");
            // 手动结构服务器的地址
            // 现在,服务器和客户端在同一台主机上,所以,运用 127.0.0.1 (环回地址 loopback address)
            // 端口运用 TranslateServer.PORT(8888)
            InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
            InetAddress remoteAddress = Inet4Address.getByName("182.254.132.183");
            DatagramPacket sentPacket = new DatagramPacket(
                    bytes, 0, bytes.length, // 要发送的数据
                    remoteAddress, TranslateServer.PORT   // 对方的 ip + port
            );
            Log.println("预备发送恳求");
            socket.send(sentPacket);
            Log.println("恳求发送完毕");
            // 3. 接纳呼应
            byte[] buf = new byte[1024];
            DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);
            Log.println("预备接纳呼应");
            socket.receive(receivedPacket);
            Log.println("呼应接纳接纳");
            byte[] data = receivedPacket.getData();
            int len = receivedPacket.getLength();
            String response = new String(data, 0, len, "UTF-8");
            String chiWord = response;
            Log.println("翻译成果: " + chiWord);
            System.out.print("请输入英文单词: ");
        }
        // 4. 封闭 socket
        socket.close();
    }
}

UDP服务端

// 供给翻译的服务器
public class TranslateServer {
    // 揭露的 ip 地址:就看进程工作在哪个 ip 上
    // 揭露的 port:需求程序中指定
    public static final int PORT = 8888;
    // SocketException -> IOException -> Exception
    public static void main(String[] args) throws Exception {
        Log.println("预备进行字典的初始化");
        initMap();
        Log.println("完结字典的初始化");
        Log.println("预备创立 UDP socket,端口是 " + PORT);
        DatagramSocket socket = new DatagramSocket(PORT);
        Log.println("UDP socket 创立成功");
        // 作为服务器,是被迫的,循环的进行恳求-呼应周期的处理
        // 等候恳求,处理并发送呼应,直到永久
        while (true) {
            // 1. 接纳恳求
            byte[] buf = new byte[1024];    // 1024 代表咱们最大接纳的数据巨细(字节)
            DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);
            Log.println("预备好接纳 DatagramPacket,最大巨细为: " + buf.length);
            Log.println("开始接纳恳求");
            socket.receive(receivedPacket); // 这个办法就会堵塞(程序履行到这儿就不动了,直到有客户发来恳求,才干继续)
            Log.println("接纳到恳求");
            // 2. 一旦走到此处,一定是接纳到恳求了,拆信
            // 拆出对方的 ip 地址
            InetAddress address = receivedPacket.getAddress();
            Log.println("对方的 IP 地址: " + address);
            // 拆出对方的端口
            int port = receivedPacket.getPort();
            Log.println("对方的 port: " + port);
            // 拆出对方的 ip 地址 + port
            SocketAddress socketAddress = receivedPacket.getSocketAddress();
            Log.println("目标的完好地址: " + socketAddress);
            // 拆出对方发送过来的数据,其实这个 data 便是咱们方才界说的 buf 数组
            byte[] data = receivedPacket.getData();
            Log.println("接纳到的目标的数据: " + Arrays.toString(data));
            // 拆出接纳到的数据的巨细(字节)
            int length = receivedPacket.getLength();
            Log.println("接纳的数据巨细(字节):" + length);
            // 3. 解析恳求 :意味着咱们需求界说自己的应用层协议
            // 首要,做字符集解码    byte[] -> String
            String request = new String(data, 0, length, "UTF-8");
            // 这个按照咱们的应用层协议
            String engWord = request;
            Log.println("恳求(英文单词):" + engWord);
            // 4. 履行事务(翻译服务),不是咱们本次演示的要点
            String chiWord = translate(engWord);
            Log.println("翻译后的成果:" + chiWord);
            // 5. 按照应用层协议,封装呼应
            String response = chiWord;
            // 进行字符集编码  String -> byte[]
            byte[] sendBuf = response.getBytes("UTF-8");
            // 6. 发送呼应
            // 作为发送方需求供给
            DatagramPacket sentPacket = new DatagramPacket(
                    sendBuf, 0, sendBuf.length,     // 要发送的数据
                    socketAddress                         // 从恳求信封中拆出来的目标的地址(ip + port)
            );
            Log.println("预备好发送 DatagramPacket 并发送");
            socket.send(sentPacket);
            Log.println("发送成功");
            // 7. 本次恳求-呼应周期完结,继续下一次恳求-呼应周期
        }
//        socket.close(); // 因为咱们是死循环,这儿永久不会走到
    }
    private static final HashMap<String, String> map = new HashMap<>();
    private static void initMap() {
        map.put("apple", "苹果");
        map.put("pear", "梨");
        map.put("orange", "橙子");
    }
    private static String translate(String engWord) {
        String chiWord = map.getOrDefault(engWord, "查无此单词");
        return chiWord;
    }
}

自界说的日志类(记得导入此类)

public class Log {
    public static void println(Object o) {
        LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String now = formatter.format(localDateTime);
        String message = now + ": " + (o == null ? "null" : o.toString());
        System.out.println(message);
    }
    public static void main(String[] args) {
        println(1);
    }
}

四、TCP数据报套接字编程

4.1 Java流套接字通讯模型

Socket套接字(网络编程万字总结-附代码)

4.2 ServerSocket API

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

4.2.1 ServerSocket 结构办法:

Socket套接字(网络编程万字总结-附代码)
服务器运用的TCP Socket目标(传入的端口,便是要揭露的端口,一般称为监听(listen)端口)

4.2. ServerSocket 一般办法

Socket套接字(网络编程万字总结-附代码)
留意

  1. accept:接起电话(服务器是电话铃响的这一方)

  2. Socket目标:树立起的衔接

    Socket套接字(网络编程万字总结-附代码)

  3. close:挂电话(谁都能够挂)

4.3 Socket API

Socket 是客户端Socket,或服务端中接纳到客户端树立衔接(accept办法)的恳求后,回来的服务端Socket。

不管是客户端仍是服务端Socket,都是两边树立衔接以后,保存的对端信息,及用来与对方收发数据的。

4.3.1 Socket 结构办法:

Socket套接字(网络编程万字总结-附代码)
留意

  1. 服务器的Socket目标是从accept()中获取到的,所以,只要客户端的Socket目标需求手动实例化出来,这个结构办法是给客户端运用,传入服务器的ip+port
  2. 一旦socket目标拿到(两边是同时拿到的),两边就方位平等了,只区分发送方和接纳方即可

4.3.2 Socket 一般办法:

Socket套接字(网络编程万字总结-附代码)
留意

  1. 输入流:站在进程视点,背面目标便是网卡,网卡抽象出来的TCP衔接,所所以给接纳方运用
  2. 输出流:同理,所所以给发送方运用

4.4 TCP中的长短衔接

TCP发送数据时,需求先树立衔接,什么时候封闭衔接就决定是短衔接仍是长衔接:

  1. 短衔接:每次接纳到数据并回来呼应后,都封闭衔接,便是短衔接。也便是说,短衔接只能一次收发数据。
  2. 长衔接:不封闭衔接,一向坚持衔接状态,两边不断的收发数据,便是长衔接。也便是说,长衔接能够屡次收发数据。

比照以上长短衔接,两者区别如下:

  • 树立衔接、封闭衔接的耗时:短衔接每次恳求、呼应都需求树立衔接,封闭衔接;而长衔接只需求第一次树立衔接,之后的恳求、呼应都能够直接传输。相对来说树立衔接,封闭衔接也是要耗时的,长衔接效率更高。
  • 主动发送恳求不同:短衔接一般是客户端主意向服务端发送恳求;而长衔接能够是客户端主动发送恳求,也能够是服务端主动发。
  • 两者的运用场景有不同:短衔接适用于客户端恳求频率不高的场景,如浏览网页等。长衔接适用于客户端与服务端通讯频频的场景,如聊天室,实时游戏等。

扩展了解:

根据BIO(同步堵塞IO)的长衔接会一向占用体系资源。关于并发要求很高的服务端体系来说,这样的耗费是不能承受的。

因为每个衔接都需求不断的堵塞等候接纳数据,所以每个衔接都会在一个线程中运行。 一次堵塞等候对应着一次恳求、呼应,不断处理也便是长衔接的特性:一向不封闭衔接,不断的处理恳求。

实际应用时,服务端一般是根据NIO(即同步非堵塞IO)来实现长衔接,功能能够极大的提升。

现在还遗留一个问题

假如同时多个长衔接客户端,衔接该服务器,能否正常处理?

需求在IDEA装备客户端支撑同时运行多个实例!

  1. 短衔接客户端 <–> 短衔接服务器 支撑同时在线
  2. 短衔接客户端 <-> 长衔接服务器 支撑同时在线
  3. 长衔接客户端 <-> 长衔接服务器 不支撑同时在线

所以能够运用多线程处理长衔接客户端不支撑同时在线的问题:

将使命专门交给其他线程来处理,主线程只担任承受socket。

4.5 代码示例(短衔接)

这儿仅演示短衔接,长衔接和多线程在博主的个人库房下:

网络开发代码

TCP服务端:

public class TranslateServerShortConnection {
    public static final int PORT = 8888;
    public static void main(String[] args) throws Exception {
        Log.println("发动短衔接版别的 TCP 服务器");
        initMap();
        ServerSocket serverSocket = new ServerSocket(PORT);
        while (true) {
            // 接电话
            Log.println("等候对方来衔接");
            Socket socket = serverSocket.accept();
            Log.println("有客户端衔接上来了");
            // 对方信息:
            InetAddress inetAddress = socket.getInetAddress();  // ip
            Log.println("对方的 ip: " + inetAddress);
            int port = socket.getPort();    // port
            Log.println("对方的 port: " + port);
            SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress();    // ip + port
            Log.println("对方的 ip + port: " + remoteSocketAddress);
            // 读取恳求
            InputStream inputStream = socket.getInputStream();
            Scanner scanner = new Scanner(inputStream, "UTF-8");
            String request = scanner.nextLine();    // nextLine() 就会去掉换行符
            String engWord = request;
            Log.println("英文: " + engWord);
            // 翻译
            String chiWord = translate(engWord);
            Log.println("中文: " + chiWord);
            // 发送呼应
            String response = chiWord;  // TODO: 呼应的单词中是没有 \r\n
            OutputStream outputStream = socket.getOutputStream();
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8");
            PrintWriter writer = new PrintWriter(outputStreamWriter);
            Log.println("预备发送");
            writer.printf("%s\r\n", response);
            writer.flush();
            Log.println("发送成功");
            // 挂掉电话
            socket.close();
            Log.println("挂断电话");
        }
//        serverSocket.close();
    }
    private static final HashMap<String, String> map = new HashMap<>();
    private static void initMap() {
        map.put("apple", "苹果");
        map.put("pear", "梨");
        map.put("orange", "橙子");
    }
    private static String translate(String engWord) {
        String chiWord = map.getOrDefault(engWord, "查无此单词");
        return chiWord;
    }
}

TCP客户端:

public class UserInputLoopShortConnectionClient {
    public static void main(String[] args) throws Exception {
        Scanner userInputScanner = new Scanner(System.in);
        while (true) {
            // 这儿做了一个假定:1)用户肯定有输入  2)用户一行一定只输入一个单词(没有空格)
            System.out.print("请输入英文单词: ");
            if (!userInputScanner.hasNextLine()) {
                break;
            }
            String engWord = userInputScanner.nextLine();
            // 直接创立 Socket,运用服务器 IP + PORT
            Log.println("预备创立 socket(TCP 衔接)");
            Socket socket = new Socket("127.0.0.1", TranslateServerShortConnection.PORT);
            Log.println("socket(TCP 衔接) 创立成功");
            // 发送恳求
            Log.println("英文: " + engWord);
            String request = engWord + "\r\n";
            OutputStream os = socket.getOutputStream();
            OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8");
            PrintWriter writer = new PrintWriter(osWriter);
            Log.println("发送恳求中");
            writer.print(request);
            writer.flush();
            Log.println("恳求发送成功");
            // 等候承受呼应
            InputStream is = socket.getInputStream();
            Scanner socketScanner = new Scanner(is, "UTF-8");
            // 因为咱们的呼应一定是一行,所以运用 nextLine() 进行读取即可
            // nextLine() 回来的数据中,会主动把 \r\n 去掉
            // TODO: 没有做 hasNextLine() 的判别
            Log.println("预备读取呼应");
            String chiWord = socketScanner.nextLine();
            Log.println("中文: " + chiWord);
            socket.close();
        }
    }
}

五、 关于输入流和输出流的运用

5.1 关于输入流的运用:

  1. 假如直接进行二进制读取
byte[] buf = new byte[1024];
int n = inputStream.read(buf);
  1. 假如读取文本数据,主张直接运用Scanner封装InputStream后再运用
Scanner S = new Scanner(inputStream,"UTF-8");
s.nextLine() ... s.hasNextLine()

5.2 关于输出流的运用:

  1. 假如直接进行二进制输出outputStream.write(buf,offset,length)
  2. 假如是文本输出,主张OutputStream -> OutputStreamWriter -> PrintWriter
OutputStreamWriter osWriter = new OutputStreamWriter(outputStream,"UTF-8");
PrintWriter writer = new PrintWriter(osWriter);
writer.println(...);
writer.print(...);
writer.printf(format,...);

重要:不要忘掉改写缓冲区,不然数据可能无法到达对方!!!

outputStream.flush();
//writer.flush();

六、面向数据报文VS面向字节省

举个栗子:

Socket套接字(网络编程万字总结-附代码)
Socket套接字(网络编程万字总结-附代码)

总结

关于端口被占用的问题:

假如一个进程A现已绑定了一个端口,再发动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。关于java进程来说,端口被占用的常见报错信息如下:

Socket套接字(网络编程万字总结-附代码)
此刻需求检查进程B绑定的是哪个端口,再检查该端口被哪个进程占用。以下为经过端口号查进程的方式:

  • 在cmd输入 netstat -ano | findstr 端口号 ,则能够显示对应进程的pid。如以下命令显 示了8888进程的pid

    Socket套接字(网络编程万字总结-附代码)

  • 在使命管理器中,经过pid查找进程

    Socket套接字(网络编程万字总结-附代码)
    处理端口被占用的问题

  1. 假如进程没啥用,就能够把进程杀掉
  2. 假如进程不确定,能够换个端口运用