简单实现网络编程

1. 前置知识

在学习网络编程前,我们需要先了解一些前置知识

1.1 客户端和服务器

在网络编程中,客户端和服务器是两个关键的角色。

客户端是发起连接并向服务器发送请求的一方。客户端通常是一个应用程序或设备,通过与服务器建立连接,发送请求并接收响应来获取所需的服务或数据。

服务器是提供服务或数据的一方。服务器通常是一个强大的计算机,它等待客户端的连接请求,并根据请求提供相应的服务或数据。服务器可以同时处理多个客户端的请求,每个请求都会分配给一个独立的线程或进程进行处理。服务器也使用特定的协议来与客户端进行通信。

1.2 请求和响应

请求(Request):请求是客户端发起的一个操作或服务请求。客户端通过发送请求给服务器,表达其需要获取某项服务、数据或执行某个操作的意图。

响应(Response):响应是服务器对客户端请求的回应。服务器接收到请求后,根据请求内容执行相应的操作,并返回相应的结果给客户端。

请求和响应之间的关系通常是一对一的。每个请求都对应着一个相应的响应。客户端发送请求后,服务器接收并处理请求,并生成相应的响应返回给客户端。客户端接收到响应后,可以根据响应的状态码和内容进行相应的处理。

也有特定情况下,存在一对多,多对一,多对多。

2. TCP/UDP 协议之间的差别

进行网络编程,本质上是使用传输层的协议提供的API接口。传输层主要有两个协议,TCP和UDP由于这两个协议之间存在一些差异,所以,它们的API也存在一些差异。这里我们先简单介绍一下TCP和UDP的差异。

TCP的特点:

  1. 有连接(Connection-Oriented):TCP是一种面向连接的协议,即在进行数据传输之前,必须先建立双方之间的连接。连接建立后,双方可以进行数据传输,传输完成后再关闭连接。

  2. 可靠传输(Reliable Transmission):TCP提供可靠的数据传输机制,保证数据的完整性、顺序性和不丢失。为了实现可靠传输,TCP采用了多种机制,如序列号、确认应答、超时重传、流量控制和拥塞控制等。通过这些机制,TCP可以检测并纠正数据传输中的错误,并确保数据按正确的顺序到达目标。

  3. 面向字节流(Byte-Oriented):TCP是一种面向字节流的协议,意味着数据在发送端和接收端之间是按照字节流的方式进行传输的,而不考虑应用层的消息边界。发送端将应用层数据分割成小块的字节流,在接收端进行重新组装。这种特性使得TCP更加灵活,可以适应不同大小的数据传输。

  4. 全双工(Full Duplex):TCP连接是全双工的,意味着数据可以在双方同时进行双向传输。发送端和接收端可以同时发送和接收数据,而且两个方向的数据流是独立的,互不影响。这种特性使得双方可以同时进行实时的双向通信,提高了传输效率。

UDP的特点:

  1. 无连接(Connectionless):UDP是一种无连接的协议,发送端和接收端之间不需要建立连接。每个UDP数据包都是独立的,可以单独发送并独立处理,不需要等待前面的数据包确认。

  2. 不可靠传输(Unreliable Transmission):与TCP不同,UDP不提供可靠的数据传输机制。UDP数据包被发送后,不会去确认是否到达目标地址,也不会进行重传操作。这意味着在网络传输过程中,可能会出现丢包、乱序或重复的情况。

  3. 面向数据报(Datagram-Oriented):UDP是一种面向数据报的协议,每个UDP数据包被视为一个独立的数据报文。每个数据报都有自己的头部信息,包含了源地址、目标地址、长度等字段。由于数据报之间是独立的,因此UDP可以灵活地处理不同大小的数据。

  4. 全双工(Full Duplex)UDP也可以在双方同时进行双向传输。

3. 网络编程

操作系统给我们提供的网络编程的 API叫做 "Socket APOI" 即 "网络编程套接字"

3.1 UDP Socket API 的使用

在Java中,其实是把操作系统提供的原生API进行封装过的,所以我们调用的API其实是JVM提供的。

使用UDP进行网络编程,核心的类有两个:

3.1.1 DatagramSocket

DatagramSocket类表示一个UDP套接字,它用于在端点之间发送和接收UDP数据包。DatagramSocket对象可以绑定到本地IP地址和端口号,以便在该地址和端口上进行监听和传输数据包(操作系统中有一类文件叫做Socket,Socket文件抽象表示了网卡这样的设备,DatagramSocket就是通过读写Socket文件,来发送和接收数据的)。

DatagramSocket的常用方法包括:

  • DatagramSocket():构造函数,创建一个绑定到本机随机端口号的DatagramSocket对象(通常用于客户端。
  • DatagramSocket(int port):构造函数创建一个绑定到本机指定端口号的DatagramSocket对象(通常用于服务器)。
  • void send(DatagramPacket p):发送指定的数据包。
  • void receive(DatagramPacket p):接收一个数据包,并将其存储在指定的DatagramPacket对象中,如果没有接收到数据报,该方法会阻塞。
  • void close():关闭DatagramSocket对象。
3.1.2 DatagramPacket

DatagramPacket类表示一个UDP数据包,它包含了要发送或接收的数据、目标地址、目标端口等信息,相当于储存数据的一个载体。DatagramPacket对象可以用于在DatagramSocket之间传递UDP数据包。

DatagramPacket的常用方法包括:

  • DatagramPacket(byte[] buf, int length):构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)。
  • DatagramPacket(byte[] buf, int length, InetAddress address, int port):构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP,port表示端口号。
  • byte[] getData():获取数据包的数据。
  • int getLength():获取数据包的长度。
  • InetAddress getAddress():获取数据包的发送端地址。
  • int getPort():获取数据包的发送端端口号。
  • void setData(byte[] buf):设置数据包的数据。
  • void setLength(int length):设置数据包的长度。
  • void setAddress(InetAddress address):设置数据包的目标地址。
  • void setPort(int port):设置数据包的目标端口号。 
3.1.3 代码示例

编写代码实现一个回显服务器,返回客户端发送的请求,即客户端发什么就返回什么。

public class UdpEchoServer {
    private DatagramSocket socket = null;//接收/发出数据
    public UdpEchoServer(int port) throws SocketException {
        //构造方法,传入端口号,指定服务器绑定的端口号
        socket = new DatagramSocket(port);
    }
    //运转逻辑,包含接收数据,返回响应
    public void start() throws IOException {
        System.out.println("服务器 启动!!!");
        while(true) {
            //请求数据报,用于接收客户端的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //调用receive方法接收数据
            socket.receive(requestPacket);
            //将接收到的数据转为字符串存储起来
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //打印出发送端的IP,端口号,和接收到的数据
            System.out.print(requestPacket.getAddress().toString() + " " + requestPacket.getPort() + ":" + request);
            //调用方法,构造出对应的响应(这个方法需自己实现)
            String response = process(request);
            //构造响应数据报,注意此处要传入对应的IP地址和端口号,可以用接收到的数据报,调用对应方法获取
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getAddress(),requestPacket.getPort());
            //发送数据
            socket.send(responsePacket);
            //打印出返回的信息
            System.out.println(" <返回>:" + response);
        }
    }
    private String process(String request) {
        //根据构造响应,我们这里直接返回request
        return request;
    }
    public static void main(String[] args) throws IOException {
        //实例化回显服务器
        UdpEchoServer udpEchoServer = new UdpEchoServer(7510);
        //调用start方法启动服务器
        udpEchoServer.start();
    }

有了服务器我们还要实现一个客户端用来发送请求:

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp = null;//目标IP地址,即服务器的地址
    private int serverPort = 0;//目标端口号,即服务器的端口号
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        //构造方法
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }
    //实现start方法用于启动服务器
    public void start() throws IOException {
        System.out.println("客户端 启动!!!");
        Scanner in = new Scanner(System.in);//用于输入请求
        while(true) {
            //输入请求
            String request = in.next();
            //构造请求数据报,注意不能直接把字符串形式的IP地址传入,需要调用InetAddress类中的getByName方法,把字符串传入
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            //发送请求
            socket.send(requestPacket);
            //接收响应,
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //用字符串储存响应
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            //输出响应
            System.out.println(">" + response);
        }
    }
    public static void main(String[] args) throws IOException {
        //创建客户端对象,注意,IP和端口号要和服务器的对应,127.0.0.0 为本机IP
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 7510);
        udpEchoClient.start();
    }
}

现在我们可以运行代码查看效果了,注意要先运行服务器,再运行客户端。

简单实现网络编程_第1张图片

简单实现网络编程_第2张图片

3.2 TCP Socket API 的使用 

UDP传输数据是无连接的,相当于 "发短信" ,TCP传输数据是有连接的,相当于 "打电话"

3.2.1 ServerSocker

ServerSocket类是用于在服务器端监听特定端口、接受客户端连接请求的类。

通过创建ServerSocket对象,可以将其绑定到指定的IP地址和端口号,从而使服务器能够监听该端口并等待客户端连接。

ServerSocket类的常用方法:

  • ServerSocket(int port):创建一个绑定到指定端口的ServerSocket对象。
  • Socket accept():监听客户端连接请求,接受客户端的连接,并返回一个Socket对象用于与客户端进行通信,如果当前没有客户端连接,该方法会阻塞。
  • void close():关闭ServerSocket对象,释放相关资源。

ServerSocket只能给服务器使用。 

 3.2.2 Socket

Socket类是用于在客户端与服务器端建立连接并进行通信的类。
通过创建Socket对象,可以指定服务器的IP地址和端口号,从而与服务器建立连接。

  • Socket(String host, int port):创建一个与指定服务器IP地址和端口号建立连接的Socket对象。
  • InputStream getInputStream():获取与Socket对象关联的输入流,用于从服务器端接收数据。
  • OutputStream getOutputStream():获取与Socket对象关联的输出流,用于向服务器端发送数据。
  • InetAddress getInetAddress():获取连接的地址。
  • int getPort():获取连接的端口号。
  • void close():关闭Socket对象,释放相关资源。
 3.2.3 代码示例

接下来我们同样使用TCP的API实现一个回显服务器,和客户端。

服务器:

public class TcpEchoServer {
    private ServerSocket socket = null;
    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器 启动!!!");
        while(true) {
            //和客户端建立连接,如果没有客户端连接,该方法会阻塞
            Socket client = socket.accept();
            //连接成功,打印信息
            System.out.printf("[%s,%d]已连接\n", client.getInetAddress(), client.getPort());
            //通过接收到的Socket对象,获取到输入,输出流
            //循环读取输入流中的需求

            try(InputStream inputStream = client.getInputStream();
            OutputStream outputStream = client.getOutputStream()) {
                while(true) {
                    //通过scanner在输入流中读取数据
                    Scanner scanner = new Scanner(inputStream);
                    if(!scanner.hasNext()) {
                        //读取完毕,说明连接断开了
                        System.out.printf("[%s,%d]已断开\n", client.getInetAddress(), client.getPort());
                        break;
                    }
                    String request = scanner.next();
                    //调用方法根据请求构造响应
                    String response = process(request);

                    //使用print的子类PrintWriter,写入响应
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //调用flush方法立刻把response写入硬盘
                    printWriter.flush();

                    //打印日志
                    System.out.printf("[%s,%d] req:%s,res:%s\n",client.getInetAddress(),client.getPort(), request, response);
                }
                //释放资源
                client.close();
            }
        }
    }
    public String process(String request) {
        //根据请求构造响应
        return request;
    }

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

客户端:

package TCP;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String ServerIp, int ServerPort) throws IOException {
        //传入服务器的 IP地址 和 端口号,在创建该对象时就会向服务器发送连接,
        // 创建完毕后等待服务器调用accept即可连接上
        socket = new Socket(ServerIp, ServerPort);
    }
    public void start() throws IOException {
        System.out.println("客户端 启动!!!");
        //连接成功,循环输入请求,获取响应
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            //从键盘接收请求
            Scanner in = new Scanner(System.in);
            //输出响应
            Scanner scanner = new Scanner(inputStream);
            //把请求输出给服务器
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true) {
                //接收请求
                System.out.print("> ");
                String request = in.next();

                //发送给服务器
                printWriter.println(request);
                //调用flush方法立刻把response写入硬盘
                printWriter.flush();

                //接受响应并输出
                String response = scanner.next();
                System.out.println(" >" + response);
            }
        }
    }

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

现在我们运行代码,发现可以正常完成功能:

简单实现网络编程_第3张图片

简单实现网络编程_第4张图片

3.2.4 改进

我们现在的代码每次只能连接一个客户端,因为我们连接了一个客户端之后,除非这个客户端断开连接,否则是出不了循环的,也就无法再次执行accept 连接新的客户端。

所以我们可以使用多线程的方法,给每个连接的客户端分配一个线程:

public class TcpEchoServer {
    private ServerSocket socket = null;
    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器 启动!!!");
        while(true) {
            //和客户端建立连接,如果没有客户端连接,该方法会阻塞
            Socket client = socket.accept();
            //连接成功,打印信息
            System.out.printf("[%s,%d]已连接\n", client.getInetAddress(), client.getPort());
            //创建一个线程,处理本次连接
            Thread t = new Thread(()->{
                //为了代码简洁,我们把处理逻辑单独封装为一个方法
                try {
                    processConnect(client);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
    public void processConnect(Socket client) throws IOException {
        try(InputStream inputStream = client.getInputStream();
            OutputStream outputStream = client.getOutputStream()) {
            while(true) {
                //通过scanner在输入流中读取数据
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()) {
                    //读取完毕,说明连接断开了
                    System.out.printf("[%s,%d]已断开\n", client.getInetAddress(), client.getPort());
                    break;
                }
                String request = scanner.next();
                //调用方法根据请求构造响应
                String response = process(request);

                //使用print的子类PrintWriter,写入响应
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //调用flush方法立刻把response写入硬盘
                printWriter.flush();

                //打印日志
                System.out.printf("[%s,%d] req:%s,res:%s\n",client.getInetAddress(),client.getPort(), request, response);
            }
            //释放资源
            client.close();
        }
    }
    public String process(String request) {
        //根据请求构造响应
        return request;
    }

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

现在我们的服务器就可以同时连接多个客户端了,我们也可以把上述代码优化为线程池的版本,节省线程大量创建和销毁带来的开销。

你可能感兴趣的:(网络)