网络编程(三) ———— Java Socket(UDP/TCP 套接字)

文章目录

  • Socket是什么?
  • TCP/UDP
  • 套接字
    • UDP套接字
      • DatagramSocket API
        • DatagramSocket构造方法
        • DatagramSocket 方法
      • InetSocketAddress API
      • 基于UDP实现回显服务器
        • 运行结果
    • TCP套接字
      • ServerSocket API
        • ServerSocket构造方法
        • ServerSocket方法
      • Socket API
        • Socket 构造方法
        • Socket方法
      • TCP中的长短连接
      • 基于TCP实现回显服务器
        • 运行结果


Socket是什么?

想知道Socket是什么就先得了解一下什么是网络编程
网络编程,通过代码来控制两个主机的进程之间能够进行数据交互。

操作系统就把网络编程的一些相关操作,封装起来了,提供了一组API供程序员使用。操作系统提供的功能,访问网络核心的硬件设备,网卡。网卡也是归操作系统来管理的

操作系统提供的socket api 是C语言风格的接口,在Java中是不能直接使用的。JDK其实也针对C语言这里的 socket API 进行了封装,在标准库中有一组类,这组类就能够让我们完成网络编程,这组类本质上仍然是调用的操作系统提供的socketAPI

操作系统,提供的 socket API主要有两类(实际上不止两类),它属于传输层

TCP/UDP

TCP和UDP这里只是简单说一下它们的特点,便于理解Socket编程,详细的会在后面的博客中写到

TCP

  • 有连接
  • 可靠传输
  • 面向字节流
  • 全双工

UDP

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 全双工

有连接:类似于微信视频,需要接通才能说话
无连接:类似于发微信消息,直接发就好了
可靠传输:发送方能知道对方是否收到消息
不可靠传输:发送方不知道是不是收到了消息

注意:可靠性 != 安全性

面向字节流:
假设发送数据为1000个字节,可以一次性发10个字节重复发100次,也可以一次发100个字节,重复发送10次,可以非常灵活的完成这里的发送,接收也是同理
TCP的文件读写都是面向字节流的

面向数据报:
以一个一个的数据报为基本单位(每个数据报多大,不同的协议里面是有不同的约定的)
发送的时候,一次至少发送一个数据报,如果尝试发送一个半,实际可能只能发出去一个
接收的时候,一次至少接收一个数据,如果尝试接收半个,剩下半个就没了

全双工:双向通信,A和B可以同时向对方发送数据
半双工:单向通信,要么A给B发,要么B给A发,不能同时发
就类似于两根水管和一根水管的区别

套接字

一个服务器的核心流程
1. 读取请求并解析
2. 根据请求计算响应
3. 把响应写回客户端

一个客户端的核心流程
1. 根据用户输入,构造请求
2. 发送请求给服务器
3. 读取服务器的响应
4. 解析响应并显示

UDP套接字

DatagramSocket API

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

DatagramSocket构造方法

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

DatagramSocket 方法

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

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

InetSocketAddress API

InetSocketAddress ( SocketAddress 的子类 )构造方法

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

基于UDP实现回显服务器

回显服务器就是客户端发送什么请求服务器就返回什么请求,UDP是不需要建立连接的

服务器代码

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

public class UdpEchoServer {
    private DatagramSocket socket;

    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }
    private void start() throws IOException {
        System.out.println("服务器启动成功");
        while (true) {
            // 1.读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            this.socket.receive(requestPacket);
            String request = new String(requestPacket.getData());
            // 2.根据请求计算响应
            String response = process(request);
            // 3.把响应返回给客户端
            DatagramPacket responsePacket = new DatagramPacket(request.getBytes(),0,request.getBytes().length,
                    requestPacket.getSocketAddress());
            this.socket.send(responsePacket);
            // 4.打印日志
            String log = String.format("[%s:%d] request: %s  response: %s",requestPacket.getAddress().toString(),requestPacket.getPort(),
                    request,response);
            System.out.println(log);
        }
    }

    /**
     * 这是一个回显服务器
     * @param request
     */
    private String process(String request) {
        //发送什么请求就返回什么响应
        return request;
    }

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

客户端代码

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

public class UdpEchoClient {
    private DatagramSocket socket;
    private int serverPort;
    private String serverIp;
    private InetSocketAddress inetSocketAddress;
    public UdpEchoClient(int port,String ip) throws SocketException {
    //客户端的IP和端口号由操作系统自动分配
        this.socket = new DatagramSocket();
        this.serverPort = port;
        this.serverIp = ip;
        this.inetSocketAddress = new InetSocketAddress(this.serverIp, this.serverPort);
    }
    public void start() throws IOException {
        System.out.println("客户端启动成功");
        Scanner sc = new Scanner(System.in);
        while (true) {
            // 1.从键盘输入请求并构造
            System.out.print("-> ");
            String request = sc.nextLine();
            if ("exit".equals(request)) {
                String log = String.format("客户端退出[%s:%d]",this.serverIp,this.socket.getPort());
                System.out.println(log);
                break;
            }
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,this.inetSocketAddress);
            // 2.把请求发送给服务器
            this.socket.send(requestPacket);
            // 3.从服务器获取响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            this.socket.receive(responsePacket);
            String response = new String(responsePacket.getData());
            // 4.打印日志
            String log = String.format("[%s:%d] request: %s    response: %s",this.serverIp,responsePacket.getPort(),request,response);
            System.out.println(log);
        }
    }

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

运行结果

网络编程(三) ———— Java Socket(UDP/TCP 套接字)_第1张图片

TCP套接字

TCP的套接字API和UDP是完全 不同的

ServerSocket API

ServerSocket 是创建TCP服务端Soket的API

ServerSocket构造方法

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

ServerSocket方法

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

Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,放回的服务端Socket
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来和对方收发数据的

Socket 构造方法

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

Socket方法

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

TCP中的长短连接

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

短连接: 每次收到数据并返回响应后,都关闭

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

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

  1. 建立连接、关闭连接的耗时

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

  2. 主动发送请求不同

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

  3. 两者的使用场景不同

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

基于TCP实现回显服务器

服务器代码

  • 创建ServerSocket 对象指定端口号
  • TCP套接字双方要先建立连接
  • 使用Thread处理多个客户端的情况
  • 每次一个Socket使用完后一定要关闭
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoServer {
    private ServerSocket listenSocket;
    public TcpEchoServer(int port) throws IOException {
        this.listenSocket = new ServerSocket(port);
    }
    private void start() throws IOException {
        System.out.println("服务器启动成功");

        while (true) {
            // TCP套接字先要建立连接
            Socket socket = this.listenSocket.accept();
            //用Thread来处理多个客户端的情况
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        connectionProcess(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
    }
    private void connectionProcess(Socket socket) throws IOException {
        try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
            Scanner sc = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            String log = String.format("[%s:%d] 客户端上线",socket.getInetAddress(),socket.getPort());
            System.out.println(log);
            while (true) {
                // 1.读取请求并解析
                if (!sc.hasNext()) {
                    log = String.format("[%s:%d] 客户端下线",socket.getInetAddress(),socket.getPort());
                    System.out.println(log);
                    break;
                }
                String request = sc.nextLine();
                // 2.根据请求计算响应
                String response = process(request);
                // 3.把响应发给客户端
                printWriter.println(response);
                //加上flush刷新缓冲区
                printWriter.flush();
                // 4.打印日志
                log = String.format("[%s:%d] request: %s  response: %s",socket.getInetAddress(),socket.getPort(),
                        request,response);
                System.out.println(log);
            }
            sc.close();
            printWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //使用后关闭,防止内存泄露
            socket.close();
        }
    }

    /**
     * 回显服务器直接返回请求
     * @param request
     * @return
     */
    private String process(String request) {
        return request;
    }

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


客户端代码

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

public class TcpEchoClient {
    private Socket clientSocket;
    private String serverIp;
    private int serverPort;

    public TcpEchoClient(int serverPort,String serverIp) throws IOException {
        this.clientSocket = new Socket(serverIp,serverPort);
        this.serverPort = serverPort;
        this.serverIp = serverIp;
    }

    public void start() {
        System.out.println("客户端启动成功");
        Scanner sc = new Scanner(System.in);
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner responseSc = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                // 1.从键盘输入请求
                System.out.print("-> ");
                String request = sc.nextLine();
                if ("exit".equals(request)) {
                    break;
                }
                // 2.发送请求给服务器
                printWriter.println(request);
                //加上flush刷新缓冲区
                printWriter.flush();
                // 3.从服务器获取响应
                String response = responseSc.nextLine();
                // 4.打印日志
                String log = String.format("[%s:%d] request: %s  response: %s",this.serverIp,this.serverPort
                ,request,response);
                System.out.println(log);
            }
            responseSc.close();
            printWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

运行结果

网络编程(三) ———— Java Socket(UDP/TCP 套接字)_第2张图片

网络编程(三) ———— Java Socket(UDP/TCP 套接字)_第3张图片

网络编程(三) ———— Java Socket(UDP/TCP 套接字)_第4张图片


下一篇 ———— 《UDP首部格式》

你可能感兴趣的:(网络编程,java,网络编程,Socket)