网络编程(三) ———— Java Socket(UDP/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();
    }
}
运行结果

在这里插入图片描述

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();
    }
}
运行结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


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

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的三毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值