【Java】网络编程

Socket套接字

概念

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基
于Socket套接字的网络程序开发就是网络编程

分类

  1. 流套接字:使用传输层TCP协议

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

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情
况下,是无边界的数据,可以多次发送,也可以分开多次接收。

  1. 数据报套接字:使用传输层UDP协议
    UDP,即User Datagram Protocol(用户数据报协议),传输层协议。

特点
无连接,不可靠传输,面向数据报,有接收缓冲区,无发送缓冲区,大小受限,一次最多64k

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

Java数据报套接字通信模型

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

java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报。

一次发送和接受UDP数据报

一次UDP数据报的发送区分发送端和接收端
发送端

第一步先创建DatagramSocket ,然后构造出要发送的内容,放在DatagramPacket数据报中(包含发送的数据,和ip地址,端口号等信息)
第二步发送数据包给接受端,通过socket.send()方法来发送给接收端
第三步接收端创建DatagramSocket,通过DatagramPacket构造出一个存放数据报的空间,通过socket.receive()方法来接受一个UDP数据报,
最后接收端获取到了发送端发送的信息

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

提供多个客户端的请求处理及响应

对于请求响应模型,客户端和服务器同时担任接收端和发送端的任务
客户端先给服务器发送请求,服务器接收到请求后执行业务逻辑,构造响应再次发送给客户端,客户端再接收响应

Java流套接字通信模型

在这里插入图片描述

Socket编程注意事项

  1. 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场
    景,一般都是不同主机。
  2. 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
  3. Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议,
    也需要考虑,这块我们在后续来说明如何设计应用层协议

UDP数据报套接字编程

DatagramSocket API

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

构造方法

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

普通方法

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

DatagramPacket API

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

构造方法

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

普通方法

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

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

InetSocketAddress API

InetSocketAddress SocketAddress 的子类 )构造方法:

InetSocketAddress(InetAddress addr, int port) 创建一个Socket地址,包含IP地址和端口号

实现回显UDP客户端和服务器

客户端

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;
    public UdpEchoClient(String ip, int port) throws SocketException {
        serverIp = ip;
        serverPort = port;
        //客户端端口号让系统自动分配
        socket = new DatagramSocket();
    }
    //让客户端反复从控制台读取用户输入的数据,把输入的数据构造成UDP请求,发送给服务器
    //在读取服务器返回的响应,打印到控制台
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("客户端启动");
        while (true){
            //1.从控制台获取用户输入的数据
            System.out.print("-->");
            String request = scanner.next();
            //构造出请求对象,发送给服务器
            /**
             * 第一个参数是数据转换为字节数组
             * 第二个参数是字节数组的长度
             * 第三个参数是ip地址
             * 第四个参数是端口号
             */
            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 {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
        udpEchoClient.start();
    }
}

服务器

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

/**
 * UDP回显服务器
 */
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) {
            //1.读取客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2.根据请求,做出响应
            String response = process(request);
            //这里是回显服务器,就返回客户端发来的请求
            DatagramPacket responsePocket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getAddress(),requestPacket.getPort());
            socket.send(responsePocket);
            System.out.printf("[%s,%d] req: %s,resp: %sn",requestPacket.getAddress().toString(),requestPacket.getPort(),
                    request,response);
        }
    }

    public String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();

    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
服务器能够进行一对多的响应

TCP流套接字编程

ServerSocket API

ServerSocket 是创建TCP服务端Socket的API

构造方法

ServerSocket(int port)
创建一个服务端流套接字Socket,并绑定到指定端口

普通方法

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

Socket API

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

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

构造方法

Socket(String host, int port)

创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

普通方法

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

TCP中的长短连接

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

短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

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

  1. 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
  2. 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
  3. 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

扩展了解:
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。

由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。

实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。

实现回显TCP客户端和服务器

服务器

import javafx.scene.layout.Priority;

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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;
    //线程数目不固定的线程池
    private ExecutorService service = Executors.newCachedThreadPool();
    public TcpEchoServer(int port) throws IOException {
        //绑定端口号
        serverSocket = new ServerSocket(port);
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = serverSocket.accept();
            //创建一个新的线程来完成任务
            //主线程用来接收客户端的连接
            //这里使用线程池,每有一个客户端请求,就使用一个线程
            service.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        //socket对象内部包含了两个字节流对象,可以把这两个字节流对象获取到,完成读写操作
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            while (true) {
                //1.读取请求并解析
                //为了读取方便,使用scanner将流对象封装
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    //读取结束,客户端下线
                    System.out.printf("[%s:%d] 客户端下线!n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.将响应发送给客户端
                //使用PrintWriter封装OutputStream
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s, resp: %sn", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            //finally中加上close操作,保证socket被关闭

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

    private String process(String resquest){
        return resquest;
    }

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

客户端

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 {
        //new操作结束后,就和服务器建立了连接
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        System.out.println("客户端启动");
        Scanner scannerConsole = new Scanner(System.in);

        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            while (true){
                System.out.print("-->");
                //1.控制台输入字符串
                String request = scannerConsole.next();
                //2.发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器获取响应
                Scanner scanner = new Scanner(inputStream);
                String response = scanner.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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