网络编程套接字

网络编程套接字


文章目录


网络编程基础

为什么需要网络编程?

  • 主要是因为网络上,有丰富的资源
  • 所谓的网络资源,其实就是在网络中可以获取各种数据资源
  • 因为需要网络来传输,所以就有了网络编程

什么是网络编程

  • 网络编程,是指网络上的主机,通过不同的进程,以编程的形式实现网络通信(或者称为网络数据传输)
  • 当然我们只需要满足不同的进程就行,即便是同一个主机,只要是不同的进程,基于网络来传输数据,也属于网络编程
  • 一般开发来说,在条件有限的情况下,一般也是在同一个主机中运行多个进程,来完成网络编程
  • 但是我们一定要明确,我们目的是提供网络上不同主机,基于网络编程来传输数据:
  1. 进程A:编程来获取资源
  2. 进程B:编程来提供资源

网络编程中的基本概念

发送端和接收端

  1. 发送端:数据的发送方进程,称为发送端,发送端主机即为源主机
  2. 接收端:数据的接收方进程,称为接收端,接收端主机即为目的主机
  3. 收发端:发送和接收端两端,即称为收发端

注意⚠️:

发送端和接收端是相对的,只是一次网络数据传输后产生数据流向的概念

请求和响应

  • 一般来说,获取一个网络资源,涉及到两次网络数据传输
  1. 第一次:请求数据发起
  2. 第二次:响应数据发起

客户端和服务端

  • 服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端
  • 客户端:获取服务的一个进程,称为客户端

对于服务来说,一般是提供

  1. 客户端获取服务端数据:
  1. 客户端资源保存在服务端:

常见的客户端服务端模型

最常见的场景,客户端是指给用户使用的程序,服务器是提供用户服务的程序:

  1. 客户端先发送请求到服务端
  2. 服务端根据请求数据,执行相应的业务处理结果
  3. 服务端返回响应:发送业务处理结果
  4. 客户端再根据响应数据,展示处理结果(展示获取的资源,或者提示保存资源的处理结果)

Socket套接字

概念

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

分类

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

  1. TCP协议

有以下特点:

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

注意⚠️:

对于字节流来说,可以简单地理解为,传输数据是基于IO流没有关闭的时候,是无边界数据,可以多次发送,也可以分开多次接收

  1. UDP协议

有以下特点:

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 全双工
  • 只有接收缓冲区,没有发送缓冲区
  • 大小受限,一次最多传输64K

注意⚠️:

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

概念解释

  1. 连接:建立通信的双方,互留信息(TCP需要双方同意才能建立通信,UDP通信直接发送数据,不需要对方同意,不保留信息)
  2. 传输:假如A向B发送信息,发送失败会采取对应措施(例如:重传),这就是可靠传输。假如不采取对应措施,就是不可靠传输
  3. 全双工:允许双向通信

注意⚠️:

可靠传输采取对应的措施,可以补救。但是机制更加复杂,传输效率会降低

Java数据报套接字模型

  • 对于UDP协议来说,具有无连接,面向数据报的特点,就是每次都是没有建立连接,并且一次传输和接收所有数据
  • Java中使用UDP协议通信,主要是基于DatagramSocket类来创建数据报的套接字,并使用DatagramPacket作为发送或者接收UDP数据报。

流程如下:

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

Java流套接字模型

  • Socket通信模型
  • Socket编程注意事项
  1. 客户端和服务器:开发时,经常是基于一个主机开启两个线程作为客户端和服务器,但真实的场景,一般是不同的主机
  2. 注意目的IP和目的端口号,标识了一次数据传输时要发送数据终点的主机和端口号
  3. Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但是应用层协议,我们也是需要考虑的,这块后面我们在说明如何设计应用层协议再说
  4. 关于端口被占用问题
  5. 如果进程A已经绑定了一个端口号,再启动一个进程B绑定该端口号,就会报错,这种就是端口被占用。对于Java来说,端口被占用的常见报错信息如下:

解决端口被占用的问题:

  • 如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B
  • 如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。

UDP数据报套接字编程

API 介绍

DatagramSocket

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

DatagramSocket 构造方法

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

DatagramSocket 方法

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

DatagramPacket

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

DatagramPacket 构造方法

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

DatagramPacket 方法

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

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

InetSocketAddress

InetSocketAddressSocketAddress 的子类)构造方法:

方法签名 方法说明
InetSocketAddress(InetAddress addr, int port) 创建一个 Socket 地址,包含 IP 地址和端口号
java 复制代码
客户端:

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

public class UdpEchoServerClient {
    private DatagramSocket socket = null;
    private String serverIp = "";
    private int serverPort = 0;

    public UdpEchoServerClient(String ip, int port) throws SocketException {
        socket = new DatagramSocket();
        serverIp = ip;
        serverPort = port;
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String request = scanner.nextLine();
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            String response = new String(requestPacket.getData(), 0, request.getBytes().length);
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServerClient server = new UdpEchoServerClient("123.0.0.1", 9090);
        server.start();
    }
}



服务器:

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

//TIP 要<b>运行</b>代码,请按 <shortcut actionId="Run"/> 或
// 点击装订区域中的 <icon src="AllIcons.Actions.Execute"/> 图标。
public class UdpEchoServer {
    private DatagramSocket socket = null;

    public void UdpEchoSever(int port) throws SocketException{
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException{
        System.out.println("服务器启动");
        while(true){
            DatagramPacket requesPacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(requesPacket);
            String request = new String(requesPacket.getData(), 0, requesPacket.getLength());
            String response = process(request);
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requesPacket.getAddress(), requesPacket.getPort());
            socket.send(responsePacket);
            System.out.println();
        }
    }

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

    public static void main(String[] args) {
        try{
            UdpEchoServer server = new UdpEchoServer();
            server.start();
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

TCP流套接字编程

API 介绍

ServerSocket

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

ServerSocket 构造方法:

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

ServerSocket 方法:

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

Socket

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

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

Socket 构造方法:

方法签名 方法说明
Socket(String host, int port) 创建一个客户端流套接字 Socket,并与对应 IP 的主机上,对应端口的进程建立连接

Socket 方法:

方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流
java 复制代码
客户端:

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(int port) throws IOException {
         socket = new Socket(serverIp, severPort);
     }

     public void start() throws IOException {
         Scanner scanner = new Scanner(System.in);
         System.out.println("Server started on port 8080");
         try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
             PrintWriter printWriter = new PrintWriter(outputStream);
             Scanner console = new Scanner(inputStream);
             while(true){
                 String line = console.next();
                 printWriter.println(line);
                 System.out.println(line);
                 printWriter.flush();
                 String response = console.nextLine();
                 System.out.println(response);
             }
         }catch(IOException e){
             e.printStackTrace();
         }
     }

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


服务器:

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;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("Server started");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            /*Thread thread = new Thread(() -> {
                processConnection(clientSocket);
            });
            thread.start();*/
            executorService.submit((new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.println("[%s:%d] 客户端上线了\n", clientSocket.getInetAddress(), clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){
            while(true){
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNextLine()){
                    break;
                }
                String request = scanner.nextLine();
                String response = process(request);
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s : %d request = %s, response = %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally{
            try{
                clientSocket.close();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
    private String process(String request){
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8080);
        server.start();
    }
}

服务器引入多线程

  • 如果是单线程没办法,同时处理多个客户端
  • 此处给每个客户端都分配一个线程
java 复制代码
// 启动服务器
public void start() throws IOException {
    System.out.println("服务器启动!");
    while (true) {
        Socket clientSocket = serverSocket.accept();
        Thread t = new Thread(() -> {
            processConnection(clientSocket);
        });
        t.start();
    }
}

服务器引入线程池

  • 避免频繁的创建和销毁线程,也可以引入线程池
java 复制代码
// 启动服务器
public void start() throws IOException {
    System.out.println("服务器启动!");
    ExecutorService service = Executors.newCachedThreadPool();
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 使用线程池, 来解决上述问题
        service.submit(new Runnable() {
            @Override
            public void run() {
                processConnection(clientSocket);
            }
        });
    }
}

长短连接

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

  1. 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据

  2. 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据

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

  • 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高

  • 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发

  • 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等

  • 扩展了解:

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

  2. 由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行

  3. 一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求

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