同系列文章导读:【JavaSE】文章导读

所有文章均在本博客首发,其他平台同步更新

如有问题,欢迎指正(评论区留言即可)

发表评论时请填写正确邮箱,以便于接收通知【推荐QQ邮箱】


网络编程入门

  • 网络编程:在网络通信协议下,不同计算机上运行的程序,可以进行数据传输
  • 网络编程三要素

    • IP地址:设备在网络中的地址,是唯一的标识
    • 端口:应用程序在设备中唯一的标识
    • 协议:数据在网络中传输的规则,常见的协议有UDP和TCP协议

IP

  • IP:全称“互联网协议地址”,也称IP地址。是分配给上网设备的数字标签。常见的IP分类为:ipv4和ipv6

    通过域名访问服务器->域名通过DNS服务器解析为IP地址传递回去->计算机再通过解析好的IP地址访问相应的服务器->服务器返回数据展示在浏览器上

  • IPv4

    • 32bit(4字节):1 100000000 10101000 00000001 01000010

      点分十进制表示法:192.168.1.66

  • IPv6

    • 由于互联网的蓬勃发展,IP地址的需求量愈来愈大,而IPv4的模式下IP的总数是有限的。

      采用128位地址长度,分成8组,每16位分为一组

    • 冒分十六进制表示法:2001:0DB8:0000:0023:0008:0800:200C:417A

      省略前面的0:2001:DB8:0:23:8:800:200C:417A

    • 特殊情况:如果计算出的16进制表示形式中间有多个连续的0(FF01:0:0:0:0:0:0:1101)

      采用0位压缩表示法:FF01::1101

  • 常用命令

    • ipconfig:查看本机IP地址
    • ping IP地址:检查网络是否连通

InetAddress的使用

  • 为了方便我们对IP地址的获取和操作,Java提供了一个类InetAddress供我们使用
  • InetAddress:此类表示Internet协议(IP)地址
  • 常用方法

    • static InetAddress getByName(String host):确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址
    • String getHostName():获取此IP地址的主机名
    • String getHostAddress():返回文本显示中的IP地址字符串

端口

  • 端口:应用程序在设备中唯一的标识
  • 端口号:用两个字节表示的整数,它的取值范围是0~65535

    其中0~1023之间的端口号用于一些知名的网络服务或者应用

    在自己使用时使用1024以上的端口号就可以了

  • 注意:一个端口号只能被一个应用程序使用

协议

  • 在计算机网络中,连接和通信的规则被称为网络通信协议
  • UDP协议

    • 用户数据报协议(User Datagram Protocol)
    • UDP是面向无连接通信协议

      速度快,有大小限制,一次最多发送64K,数据不安全,容易丢失数据

  • TCP协议

    • 传输控制协议(Transmission Control Protocol)
    • TCP协议是面向连接的通信协议

      速度慢,没有大小限制,数据安全


UDP通讯程序

  • 发送端:发送数据

    接收端:接收数据

UDP发送端

  • 发送数据步骤

    • 创建发送端的DatagramSocket对象
    • 创建数据,并把数据打包(DatagramPacket)
    • 调用DatagramSocket对象的方法发送数据
    • 释放资源
  • 代码实现
public class Demo{
    public static void main(String[] args) throws IOException{
        // 创建发送端对象
        DatagramSocket ds = new DatagramSocket();
        // 数据打包   DatagramPacket(byte[] buf, int length, InetAddress, int port)
        String s = "需要发送的数据";
        byte[] bytes = s.getBytes();
        InetAddress address = InetAddress.getByName("127.0.0.1");
        int port = 10000;
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
        // 发送数据
        ds.send(dp);
        // 释放资源
        ds.close();
    }
}

UDP接收端

  • 步骤

    • 创建接收端的DatagramSocket对象
    • 创建一个箱子,用于接收数据
    • 调用DatagramSocket的方法接收数据并将数据放入箱子中
    • 解析数据包,并把数据在控制台显示
    • 释放资源
  • 代码
public class Demo{
    public static void main(String[] args) throws{
        // 创建接收端对象------参数表示从10000端口接收数据
        DatagramSocket ds = new DatagramSocket(10000);
        // 创建箱子
        byte[] bytes = new bytes[1024];
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
        // 接收数据
        ds.receive(dp);
        // 获取数据
        byte[] data = dp.getData();
        System.out.println(new String(data));
        // 释放资源
        ds.close();
    }
}

在这里运行时必须先运行接收端再运行发送端(否则都发送完了才运行接收端就接收不到了)

如果接收端在启动之后没有接收到数据,会阻塞

在接收数据的时候,需要调用一个getLength方法,表示接收到了多少字节

UDP练习

  • UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束
  • UDP接收端:因为不知道发送端什么时候停止发送,就死循环接收
// 发送端
package UDP;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;

public class ClientDemo {
    public static void main(String[] args) throws IOException{
        Scanner sc = new Scanner(System.in);
        DatagramSocket ds = new DatagramSocket();
        while(true){
            String s = sc.nextLine();
            if("886".equals(s)) break;
            byte[] bytes = s.getBytes();
            InetAddress address = InetAddress.getByName("127.0.0.1");
            int port = 10000;
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
            ds.send(dp);
        }
        ds.close();
    }
}
// 接收端
package UDP;

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

public class ServerDemo {

    public static void main(String[] args) throws IOException {
        DatagramSocket ds = new DatagramSocket(10000);
        while(true){
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
            ds.receive(dp);
            byte[] data = dp.getData();
            int length = dp.getLength();
            System.out.println(new String(data, 0, length));
        }
        // ds.close();
    }
}

三种通信方式

  • 单播:一个发送端通过一个路由器只发送给一个接收端(一对一)
  • 组播:一个发送端通过一个路由器发送给一组接收端(一对多)

    • 组播地址:224.0.0.0~239.255.255.255

      其中224.0.0.0~224.0.0.255为预留的组播地址,不可用

    • 组播的发送端在调用DatagramSocket对象的方法发送数据时,单播是发送给指定IP的电脑,组播是发送给组播地址
    • 组播的接收端创建的是MulticastSocket对象

      在创建箱子后,还要把当前电脑添加到这一组中

// 组播的发送端
public static void main(String[] args) throws IOException{
    DatagramSocket ds = new DatagramSocket();
    String s = "Hello Word";
    byte[] bytes = s.getBytes();
    InetAddress address = InetAddress.getByName("224.0.1.0");
    int port = 10000;
    DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
    ds.send(dp);
    ds.close();
}
// 接收端
public static void main(String[] args) throws IOException{
    MulticastSocket ms = new MulticastSocket(10000);
    DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
    // 把当前计算机绑定一个组播地址
    ms.joinGroup(InetAddress.getByName("224.0.1.0"));
  
    // 接收数据
    ms.receive(dp);
    byte[] data = dp.getData();
    int length = dp.getLength();
    System.out.println(new String(data, 0, length));
    ms.close();
}
  • 广播:一个发送端通过一个路由器发送给该局域网下所有接收端(一对所有)

    • 广播地址:255.255.255.255
// 发送端
public static void main(String[] args) throws IOException{
    DatagramSocket ds = new DatagramSocket();
    String s = "广播发送端";
    byte[] bytes = s.getBytes();
    InetAddress address = InetAddress.getByName("255.255.255.255");
    int port = 10000;
    DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
    ds.sent(dp);
    ds.close();
}
// 接收端
public class ServerDemo {

    public static void main(String[] args) throws IOException {
        DatagramSocket ds = new DatagramSocket(10000);
        while(true){
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
            ds.receive(dp);
            byte[] data = dp.getData();
            int length = dp.getLength();
            System.out.println(new String(data, 0, length));
        }
        ds.close();
    }
}

TCP通讯程序

  • TCP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket对象

    发送端:Socket   接收端:ServerSocket

    通信之前要保证连接已经建立

    通过Socket产生IO流来进行网络通信

发送数据

  • 创建客户端的Socket对象(Socket)与指定服务端连接

    Socket(String host, int port)

  • 获取输出流,写数据

    OutputStream getOutputStream()

  • 释放资源

    void close()

  • 代码实现
// 客户端
public static void main(String[] args) throws IOException{

  Socket socket = new Socket("127.0.0.1", 10000);

  OutputStream os = socket.getOutputStream();
  os.write("hello".getBytes());

  os.close();
  socket.close();
}

接收数据

  • 创建服务端的Socket对象(ServerSocket)

    ServerSocket(int port)

  • 监听客户端连接,返回一个Socket对象

    Socket accept()

  • 获得输入流,读数据,并把数据显示在控制台

    InputStream getInputStream()

  • 释放资源

    void close()

  • 代码实现
// 服务端
public static void main(String[] args){
  ServerSocket ss = new ServerSocket(10000);

  Socket accept = ss.accept();        // 没有客户端连接的话,就会死等,不执行后续代码,即阻塞

  InputStream is = accept.getInputStream();
  int b;
  while((b = is.read()) != -1){
      System.out.print((char) b);
  }

  is.close();
  ss.close();
}

原理分析

  • 不能先运行客户端(先运行客户端会没有服务端可连,就会报错)
  • 先运行服务端,代码会依次执行到accept方法,然后就阻塞,等待客户端连接
  • 运行客户端,在对象创建完毕之后,客户端与服务端的连接通道就已经建立

    客户端写数据,服务端读数据

  • 客户端释放资源,服务端释放资源

客户端创建对象并连接服务器,此时是通过三次握手协议保证服务器之间的连接

针对客户端,是往外写的,所以是输出流;而服务端是往进读的,所以是输入流

read方法也是阻塞的

在关流的时候,还多了一个往服务器写结束标记的动作

最后一步断开连接,会通过四次挥手协议保证连接终止

三次握手

  • 保证客户端与服务器之间建立连接
  • 第一次:客户端向服务器发出连接请求(等待服务器确认)
  • 第二次:服务器向客户端返回一个响应(告诉客户端收到了请求)
  • 第三次:客户端向服务端再次发出确认信息(连接建立)

四次挥手

  • 保证客户端与服务端取消连接,成功终止连接
  • 第一次:客户端向服务器发出取消连接请求
  • 第二次:服务器向客户端返回一个响应,表示收到客户端取消请求

    服务器将最后的数据处理完毕

  • 第三次:服务器向客户端发出确认取消信息
  • 第四次:客户端再次发送确认消息(连接取消)

练习

练习一

  • 客户端:发送数据,接收服务器反馈
  • 服务器:接收数据,给出反馈
  • 代码
// 客户端
package TCP;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.io.OutputStream;


public class ClientDemo {

    public static void main(String[] args) throws IOException {
            Socket socket = new Socket("127.0.0.1", 10000);

            OutputStream os = socket.getOutputStream();
            os.write("hello".getBytes());
            socket.shutdownOutput();    // 仅仅关闭输出流,并写一个结束标记,对socket没有任何影响

            InputStream is = socket.getInputStream();
            int b;
            while((b = is.read()) != -1){
                    System.out.print((char)b);
            }

            is.close();
            os.close();
            socket.close();

    }
}
// 服务端
package TCP;

import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import java.io.InputStream;

public class ServerDemo {

    public static void main(String[] args) throws IOException {
            ServerSocket ss = new ServerSocket(10000);

            Socket accept = ss.accept();

            InputStream is = (InputStream) accept.getInputStream();
            int b;
            while((b = is.read()) != -1){
                    System.out.print((char)b);
            }

            OutputStream os = accept.getOutputStream();
            os.write("who?".getBytes());

            os.close();
            is.close();
            accept.close();
            ss.close();

    }
}

练习二

  • 客户端:将本地文件上传到服务器,接收服务器的反馈
  • 服务器:接收客户上传的文件,上传完毕之后给出反馈
  • 代码
// 客户端
package TCP;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

public class ClientDemo2 {

    public static void main(String[] args) throws IOException {
            Socket socket = new Socket("127.0.0.1", 10000);

            // 本地的流,读取本地文件
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream("1.png"));

            OutputStream os = socket.getOutputStream();
            BufferedOutputStream bos = new BufferedOutputStream(os);
            int b;
            while((b = bis.read()) != -1){
                    bos.write(b);        // 通过网络写到服务器中
            }

            // 给服务器一个结束标记
            socket.shutdownOutput();

            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line;
            while((line = br.readLine()) != null){
                    System.out.println(line);
            }

            socket.close();
            bis.close();
    }

}
// 服务器
package TCP;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo2 {

    public static void main(String[] args) throws IOException {
            ServerSocket ss = new ServerSocket(10000);

            Socket accept = ss.accept();
            // 网络中的流,从客户端读取数据的
            BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
            // 本地的IO流,把数据写到本地中,实现永久化存储
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("src\\copy.png"));

            int b;
            while((b = bis.read()) != -1){
                    bos.write(b);
            }
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
            bw.write("上传成功!");
            bw.newLine();
            bw.flush();

            accept.close();
            ss.close();
            bos.close();
    }

}

服务端优化

  • 第一个弊端:服务器只能处理一个客户端请求,接受完一个文件之后,服务器就关闭了

    改进方式:循环

  • UUID

    • UUID uuid = UUID.randomUUID(); 生成一个随机且唯一的uid
    • uuid.toString(); 转换为字符串
    • 防止多次上传文件时,后上传的文件覆盖掉先上传的文件
  • 仅仅使用while循环,无法同时处理多个客户端的请求

    采用多线程改进

  • 使用多线程虽然可以让服务器同时处理多个客户端请求,但是资源消耗太大

    采用线程池改进


推荐阅读:【JavaSE】基础加强

最后修改:2022 年 04 月 16 日
如果觉得我的文章对你有用,请随意赞赏