12 网络编程详解

知识预备

  • 网络通信 :

​ 网络通信是指,将一台设备(Host1)中的数据通过网络传输到另一台设备(Host2)。java.net 包下提供了许多用于完成网络通信的类或接口。

​ 相关流程图如下 :

  • 网络 :

​ 两台或两台以上的设备通过一定物理设备(交换机,网关服务器等等)连接起来构成了网络(强调多台计算机构成的网状结构)。

​ 根据网络的覆盖范围及其复杂度的不同,网络大致分为三类:

1° 局域网 : 覆盖范围最小,可能是一个机房,一个公司的内网,也可能是一个学校的校园网。

2° 城域网 : 覆盖范围较大,可以覆盖整个城市。

3° 广域网 : 覆盖范围最大,可以覆盖整个国家,甚至覆盖全球。典型代表是万维网(World Wide Web,简称3w)。

  • IP :

​ IP 全称 Internet Protocol,网络协议;IP地址就是“IP address”。IP的作用是标识网络中的每台计算机(主机)。

​ 在Windows操作系统下,可以在交互式命令接口(cmd)输入 ipconfig 指令来获取当前主机的ip地址。如下图所示 :

​ ip又常见两种—— IPv4 和 IPv6.

1° IPv4协议 以“点分十进制(xx.xx.xx.xx)”表示ip地址。即用4个字节来表示4个数,每个数的范围就是每个字节可以表示的范围(0~255)。

IPv4协议下,ip地址由网络地址 + 主机地址组成,比如上图红框中,“192.168.0.”表示网络地址,最后的“116”表示主机地址。

2° IPv6协议 的诞生是为了解决IPv4协议“网络地址资源受限”问题,它是互联网工程任务组设计的 用于替代IPv4的下一代网络协议(目前仍处于替代的过渡阶段)。IPv6的地址数量,号称可以为全世界的每一粒沙子编上一个地址。IPv6 的诞生也解决了多种接入设备接入互联网的障碍。

​ IPv6协议下,ip地址一般由16进制表示,因此每两位就代表一个字节,每个IPv6协议下的ip地址均有16个字节(128位)表示,长度是IPv4协议下的四倍。

  • IPv4的ip地址分类:

​ IPv4协议下,ip地址由网络地址和主机地址构成,根据它们的权重不同,IPv4协议的ip地址可分为A,B,C,D,E五类。如下图所示 :

​ PS : 127.0.0.1表示本机地址

  • 域名和端口 :

1° 域名——公司拥有自己的主机,为了便于用户访问公司主机,通过诸如HTTP协议将主机的ip地址映射为“域名”。eg : "cn.bing.com"就是必应搜索的域名。域名相比ip地址更易记忆。

2° 端口——用于标注计算机上某个特定的网络程序(网络服务)的编号。不同的网络程序一般监听不同的端口。

端口以整数形式表示,表示范围是065535(2个字节表示,02^16 - 1)。其中,01024的端口已经被占用,因此,在网络开发中,不建议使用01024的端口,eg:ssh--- 22,ftp(文件传输) --- 21,smtp(邮件) --- 25,http --- 80。

常见网络程序的端口号——

​ ①mysql : 3306;

​ ②sqlserver : 1433;

​ ③oracle : 1521;

​ ④tomcat : 8080;

PS : 用户访问业务主机,实际是通过IP + 端口的形式访问的;通过IP可以找到主机所在位置,通过端口可以使用主机上提供的对应网络服务。如下图所示 :

  • 网络协议:

网络通信协议,通常指 TCP/IP 协议(全称“Transmission Control Protocol / Internet Protocol协议”)。中文是“传输控制协议 / 网际协议”,又叫网络通信协议TCP/IP协议是Internet最基本的协议,是Internet国际互联网络的基础。简单地说,就是由网络层的IP协议和传输层的TCP协议组成。

关于TCP和UDP的区别——

1° TCP : 传输控制协议

使用TCP协议前,需要建立TCP连接,形成数据传输通道;

传输数据前进行了"三次握手",因此是可靠的.

在连接中可进行大量数据的传输;

数据传输完毕后, 需释放掉已建立的连接,因此效率低.

2° UDP : 用户数据报协议

不需要建立连接, 而是将数据, 源, 目的封装成数据报;

每个数据报的大小限制在了64kb内, 不适合大量数据的传输;

因无需连接, 因此不可靠;

数据传输完毕后, 无需释放资源(因为不面向连接), 因此速度快.


网络编程

  • InetAddress :

​ 1° 常用方法 :

①getLocalHost : 获取当前主机的InetAddress对象,直接打印InetAddress对象默认输出结果为当前主机名 + 当前主机的ip地址(IPv4协议)。

②getByName : 通过指定主机名获取对应的InetAddress对象。

③getByName : 同上;也可以通过指定域名来获取对应的InetAddress对象。

④getHostAddress : 返回当前InetAddress对象的IP地址。

⑤getHostName : 返回当前InetAddress对象的主机名/域名。

​ 2° 代码演示 :

​ 以InetAddress_Demo类为演示类,代码如下 :

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddress_Demo {

    public static void main(String[] args) {
        try {
            // 1. 获取本机的 InetAddress 对象
            InetAddress localHost = InetAddress.getLocalHost();
            System.out.println(localHost);
            System.out.println("=======================");

            // 2. 通过指定主机名,获取本机的 InetAddress 对象
            InetAddress byName = InetAddress.getByName("MS-BJCMJBNFQLQN");
            System.out.println(byName);
            System.out.println("=======================");

            // 3. 通过指定域名,获取对应的 InetAddress 对象
            InetAddress baiduHost = InetAddress.getByName("www.baidu.com");
            System.out.println(baiduHost);
            System.out.println("=======================");

            // 4. 根据 InetAddress 对象,获取对应主机的IP地址
            String hostAddress = baiduHost.getHostAddress();
            String localAddress = localHost.getHostAddress();
            System.out.println(hostAddress);
            System.out.println(localAddress);
            System.out.println("=======================");

            // 5. 根据InetAddress对象,获取对应主机的主机名/域名(最终获取到的结果取决于主机的配置)
            String baiduHostName = baiduHost.getHostName();
            String hostName = localHost.getHostName();
            System.out.println(baiduHostName);
            System.out.println(hostName);
            System.out.println("=======================");

        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }

}

运行结果 :

  • Socket :

概述 :

Socket在网络编程中被称为“套接字”;Socket在开发网络应用程序中被广泛采用,因此成为了事实上的标准。

Socket是两台机器间实现通信的端点(比喻为"接口"),通信的两端都要有Socket。网络通信其实就是Socket间的通信

Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。一般主动发起通信的应用程序属于客户端;等待通信请求的属于服务端。(客户端程序和服务端程序一般位于不同的主机上)。

服务端要先运行起来, 等待客户端的连接。

代码演示 :

eg1 : 客户端连接服务端

​ Server_01代码如下 :

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
    TCP InputStream / OutputStream No.1
 */
public class Server_01 {
    public static void main(String[] args) throws IOException {
        //1.ServerSocket可以监听指定的端口(要求该端口没有被其他服务端程序监听)
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务端正监听6666端口,等待连接中......");
        //2.当有客户端连接被监听的端口时,返回Socket对象,程序继续运行。
        /*
            ServerSocket可通过accept方法返回多个Socket对象,
            适用于多个服务器连接客户端时的并发。
         */
        Socket socket = serverSocket.accept();
        System.out.println("Server's socket = " + socket);
        //3.若有客户端成功连接该服务端,读取客户端写入到数据通道中的数据, 并显示在控制台
        InputStream inputStream = socket.getInputStream(); 
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) != -1) {
        	System.out.println(new String(buffer, 0, len));
        }
        
        //4.必须关闭流(服务端比客户端多关一个)
        byteArrayOutputStream.close();
        inputStream.close();
        socket.close();
        serverSocket.close();
    }
}

​ Client_01代码如下 :

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
/**
    TCP InputStream / OutputStream No.1
 */
public class Client_01 {
    public static void main(String[] args) throws IOException {
        /*
            1.通过“IP + 端口” 连接服务端。
            2.若连接成功,返回Socket对象。
         */
        Socket socket = new Socket(InetAddress.getLocalHost(), 6666);
        System.out.println("Client's socket = " + socket);
        /*
            3.连接成功后,通过Socket类对象的getOutputStream()方法,
              获取和socket对象关联的字节输出流对象。
         */
        OutputStream outputStream = socket.getOutputStream();
        /*
            4.利用获取到的的字节输出流对象,将指定内容输出到数据传输流中。
         */
        outputStream.write("Cyan_RA9".getBytes());  //一次写入一个字节数组
        System.out.println("客户端退出......");
        //5.必须关闭流。
        outputStream.close();
        socket.close();
    }
}

运行效果 :

eg2 : 结束标记

​ Server_02代码如下 :

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
    TCP InputStreamReader / OutputStreamWriter No.2
 */
public class Server_02 {
    public static void main(String[] args) throws IOException {
        //1.设置服务端程序的监听端口为8888
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服务端程序正在监听8888端口,等待客户端连接......");
        //2.当有客户端连接成功时,通过accept方法返回Socket类对象
        Socket socket = serverSocket.accept();
        System.out.println("Server's socket = " + socket);
        //3.获取与socket对象关联的字节输入流对象,并利用转换流将其转换为字符输入流。
        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);
        //4.利用转换得到的字符输入流,读取客户端写入到数据通道中的数据。
        int data;
        while ((data = isr.read()) != -1) {
            System.out.print((char)data);
        }
        //5.获取与socket对象关联的字节输出流对象,并利用转换流将其转换为字符输出流。
        OutputStream outputStream = socket.getOutputStream();
        OutputStreamWriter osw = new OutputStreamWriter(outputStream);
        //6.利用转换得到的字符输出流对象,将数据写入到数据通道中。
        osw.write("Hello, Client? ");
        //若使用字符流,需要使用flush方法手动刷新,否则数据无法写入数据通道!
        osw.flush();
        socket.shutdownOutput();    //设置写入结束标记
        //7.关闭流
        osw.close();
        isr.close();
        socket.close();
        serverSocket.close();
        System.out.println("\n服务端退出......");
    }
}

​ Client_02代码如下 :

package csdn.advanced.netEX.tcp_2;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
/**
    TCP InputStreamReader / OutputStreamWriter No.2
 */
public class Client_02 {
    public static void main(String[] args) throws IOException {
        //1.通过“IP + 端口”连接服务端,若连接成功,返回Socket类对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        System.out.println("Client's socket = " + socket);
        //2.获取与socket对象关联的字节输出流对象,并利用转换流,将其转换为字符输出流。
        OutputStreamWriter osw =
                new OutputStreamWriter(socket.getOutputStream());
        /**
         * 也可以使用BufferedWriter再包装一层,
         * 使用处理流之后,可以使用newLine方法作为结束的标志,但要求读取使用readLine方法。
         *  */
        //3.利用转换得到的字符输出流对象,向数据通道中写入数据。
        osw.write("Hello, Server?");
        osw.flush();
        /*
            若使用字符流,需要使用flush方法手动刷新,否则数据无法写入数据通道!
            shutdownOutput() 方法可以告诉服务端,发送的信息到此为止了。
         */
        socket.shutdownOutput();
        //4.获取与socket对象关联的字节输入流对象,并利用转换流,将其转换为字符输入流。
        InputStreamReader isr =
                new InputStreamReader(socket.getInputStream());
        //5.利用转换得到的字符输入流对象,读取服务端写入到数据通道中的数据。
        char[] data = new char[1024];
        int cnt;
        while ((cnt = isr.read(data)) != -1) {
            String string = new String(data, 0, cnt);
            System.out.println(string);
        }
        //6.关闭流
        isr.close();
        //osw.close();
        socket.close();
        System.out.println("客户端退出......");
    }
}

运行结果 :

eg3 : 网络传输文件

*以传入某图片为例*如下所示 :

​ Server_03类代码如下 :

package csdn.advanced.netEX.tcp_3;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
    TCP --- network transmission
    advanced --- BufferedXxx
 */
public class Server_03 {
    public static void main(String[] args) throws IOException {
        //1.客户端监听端口
        ServerSocket serverSocket = new ServerSocket(7777);
        System.out.println("服务端正监听7777端口,等待客户端连接...");
        Socket socket = serverSocket.accept();
        System.out.println("Server's socket = " + socket);
        //2.获取与socket对象关联的字节输入流,读取客户端写入到数据通道中的数据。
        InputStream inputStream = socket.getInputStream();
        OutputStream outputStream = socket.getOutputStream();//4.步骤
        BufferedOutputStream bos =
                new BufferedOutputStream(new FileOutputStream("D:\\JAVA\\IDEA\\fileEX\\demo2.jpg"));
        byte[] data = new byte[1024];
        int cnt;
        //3.将读取到的图片写入到本地
        while ((cnt = inputStream.read(data)) != -1) {
            bos.write(data, 0, cnt);
        }
        //4.向客户端发送“收到图片”,并退出服务端。
        outputStream.write("Received the picture".getBytes());
        socket.shutdownOutput();    //结束标记
        //5.不要忘记关闭流(后打开的流先关闭)
        bos.close();
        outputStream.close();
        inputStream.close();
        socket.close();
        serverSocket.close();
        System.out.println("服务端退出...");
    }
}

​ Client_03类代码如下 :

package csdn.advanced.netEX.tcp_3;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
/**
 * TCP --- network transmission
 */
public class Client_03 {
    public static void main(String[] args) throws IOException {
        //1.通过“IP + 端口”,连接服务端。
        Socket socket = new Socket(InetAddress.getLocalHost(), 7777);
        System.out.println("Client's socket = " + socket);
        //2.向服务端发送一张图片(把从客户端读取到的文件写入到数据通道中)
        BufferedInputStream bis =
                new BufferedInputStream(new FileInputStream("D:\\JAVA\\IDEA\\fileEX\\demo.jpg"));
        InputStream inputStream = socket.getInputStream();//3.步骤
        OutputStream outputStream = socket.getOutputStream();
        byte[] data = new byte[1024];
        int cnt;
        while ((cnt = bis.read(data)) != -1) {
            outputStream.write(data, 0, cnt);
        }
        socket.shutdownOutput();    //结束标记
        //3.获取与socket对象关联的字节输入流,读取服务端写入到数据通道中的信息
        byte[] message = new byte[1024];
        while ((cnt = inputStream.read(message)) != -1) {
            String s = new String(message, 0, cnt);
            System.out.println(s);
        }
        //4.关闭流,客户端退出
        outputStream.close();
        inputStream.close();
        bis.close();
        socket.close();
        System.out.println("客户端退出...");
    }
}

运行效果 :

3. netstat:

netstat -an 指令可以查看当前网络情况,包括端口监听和网络连接情况;

netstat -an | more 指令可以分页查看当前网络情况(按下空格翻页,直到显示完毕,也可以通过Ctrl + c快捷键直接退出指令)。

netstat 指令需要在DOS系统下执行(小黑框)。

​ 如下图所示 :

​ 其中——

Proto = Protocol,表示当前端口的协议。

Local Address 和 Foreign Address 分别表示本机地址(服务端)和外部地址(客户端), 前四位表示IP,第五位表示监听的端口。

State表示状态,Listening 表示当前程序正在指定端口进行监听,等待客户端连接Established 表示当前程序监听的端口已有客户端连接。

如果想查看正在监听端口的具体程序,需要使用管理员权限运行DOS,然后使用 netstat -anb 指令,如下图所示 :

PS : 在客户端与服务端连接期间调用 netstatn 指令,可以得到结论——

当客户端连接到服务器端后,实际上客户端也是通过一个端口与服务端进行通讯的,这个端口是 TCP/IP 协议来自动分配的

4. UDP网络通信 :

DatagramSocket类 和 DatagramPacket(数据报)类 实现了基于UDP协议的网络通讯。UDC数据报是通过 DatagramSocket 套接字的 send 和 receive 方法来进行发送和接收的;系统不保证数据报具体的发送情况。(一个数据报最大64kb)

DatagramPacket对象中封装了数据报,其中包含了数据、发送端的IP + 端口以及接收端的IP + 端口。正因为每个数据报中都给出了完整的地址信息,因此UDP无需像TCP一样建立连接。DatagramPacket类构造器如下 :

PS :

在UDP网络通信中不再有服务端和客户端的概念,而是数据的发送端和接收端,并且发送端和接收端不是固定的

UDP编程中,接收数据(receive) 和 发送数据(send) 是通过DatagramSocket对象来完成的。

在发送数据时,会先将数据打包到DatagramPacket对象中在接收数据时,需要先对DatagramPacket对象进行拆包。(拆包——getLength(),getData(),String带参构造)

DatagramPacket对象可以指定端口来接收数据。

最后需要关闭DatagramSocket

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UdpClientDemo01 {
   public static void main(String[] args) {

       try {
           // 1.建立一个Socket
           DatagramSocket datagramSocket = new DatagramSocket();
           // 2.建个包
           String msg = "你好啊,服务器!";

           DatagramPacket datagramPacket = new DatagramPacket(msg.getBytes(),0,msg.getBytes().length,InetAddress.getLocalHost(),9999);
           // 3.发送包
           datagramSocket.send(datagramPacket);

           datagramSocket.close();
       } catch (IOException e) {
           throw new RuntimeException(e);
       }
   }
}

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

public class UdpServerDemo01 {
    public static void main(String[] args) {
        try {
            // 开放端口
            DatagramSocket datagramSocket = new DatagramSocket(9999);
            // 接收数据包
            byte[] buffer = new byte[1024];
            DatagramPacket datagramPacket = new DatagramPacket(buffer, 0, buffer.length);
            datagramSocket.receive(datagramPacket);
            System.out.println(datagramPacket.getAddress().getHostAddress());
            System.out.println(new String(datagramPacket.getData(),0,datagramPacket.getLength()));

            // 关闭连接
            datagramSocket.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行效果图:


实战

基于 UDP 实现多线程在线咨询

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;

public class TalkSend implements Runnable {

    DatagramSocket socket;
    BufferedReader reader;

    private int fromIP;
    private String toIP;
    private int toPort;

    public TalkSend(int fromIP, String toIP, int toPort) {
        this.fromIP = fromIP;
        this.toIP = toIP;
        this.toPort = toPort;

        try {
            socket = new DatagramSocket(this.fromIP);
            reader = new BufferedReader(new InputStreamReader(System.in));
        } catch (SocketException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                String message = reader.readLine();
                byte[] data = message.getBytes();
                DatagramPacket packet = new DatagramPacket(data, 0, data.length, new InetSocketAddress(this.toIP, this.toPort));
                socket.send(packet);
                // 断开连接条件,bye
                if (message.equals("bye")) {
                    break;
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        socket.close();
    }
}

这段代码定义了一个名为 TalkSend 的类,它实现了 Runnable 接口,用于通过 UDP(用户数据报协议)进行数据的发送操作。该类可以从控制台读取用户输入的消息,并将这些消息封装成 UDP 数据报发送到指定的 IP 地址和端口。当用户输入 "bye" 时,程序会停止发送消息并关闭 DatagramSocket

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

public class TalkReceive implements Runnable {
    DatagramSocket socket;
    private int port;
    private String msgFrom;

    public TalkReceive(int port, String msgFrom) {
        this.port = port;
        this.msgFrom = msgFrom;
        try {
            socket = new DatagramSocket(this.port);
        } catch (SocketException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 准备接收包裹
                byte[] buffer = new byte[1024];
                DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
                socket.receive(packet);

                // 打印接收的信息
                byte[] data = packet.getData();
                String recevieData = new String(data, 0, packet.getLength());
                System.out.println(msgFrom+": "+recevieData);

                // 断开连接条件,bye
                if (recevieData.equals("bye")) {
                    break;
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        socket.close();
    }
}

这段代码定义了一个名为 TalkReceive 的类,它实现了 Runnable 接口。该类的主要功能是通过 UDP(用户数据报协议)接收网络消息,并将接收到的消息打印输出。当接收到的消息内容为 "bye" 时,程序会停止接收消息并关闭 DatagramSocket

public class TalkStudent {
    public static void main(String[] args) {
        // 开启两线程
        // 1. 学生往 “localhost + 4444” 发送消息
        TalkSend talkSend = new TalkSend(5555, "localhost", 4444);
        // 2. 学生接收发往 “3333” 的消息 即:老师所发的消息
        TalkReceive talkReceive = new TalkReceive(3333, "老师");
        new Thread(talkSend).start();
        new Thread(talkReceive).start();

    }
}
public class TalkTeacher {
    public static void main(String[] args) {
        // 开启两个线程
        // 1. 老师往 “localhost + 3333” 发送消息
        TalkSend talkSend = new TalkSend(6666, "localhost", 3333);
        // 2. 老师接收发往 “4444” 的消息 即:学生所发的消息
        TalkReceive talkReceive = new TalkReceive(4444, "学生");
        new Thread(talkSend).start();
        new Thread(talkReceive).start();
    }
}

效果图:

posted @ 2025-02-05 00:19  没关系都是……  阅读(44)  评论(0)    收藏  举报