网络编程套接字-Socket

网络编程套接字

socket,是操作系统给应用程序(传输层给应用层)提供的api,Java也对这个api进行了封装。

socket提供了两组不同的api,UDP有一套,TCP有一套,本文主要介绍api的使用

TCP、UDP的区别:

TCP:有连接、可靠传输、面向字节流、全双工

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

有连接和无连接:通信双方保存了通信对方的信息就是有连接,如果不保存就是无连接。

可靠和不可靠:可靠就是确保数据尽可能到达对方,而不可靠就是完全不考虑数据是否能够到达对方。

面向字节流和面向数据报:字节流和文件流的特点是一样的;面向数据报是传输数据报,而不是单个字节。

全双工:通信双方都可以发送和接收数据,双向通信。

UDP、TCP的api使用

UDP的api

DatagramSocket、DatagramPacket

DatagramSocket代表一个Socket对象,操作系统把网卡这种硬件设备的概念封装成socket,操作socket对象就能间接操控网卡,相当于网卡的遥控器。

DatagramPacket代表一个UDP数据报

网络编程需要操作网卡,需要用到socket对象

DatagramSocket:

构造方法:

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

方法:

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

DatagramPacket:

构造方法:

方法名 说明
DatagramPacket(byte[] buf,int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组buf中,接收指定的长度(length)
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组buf中,接收指定的长度(length),address表示指定的目的主机的ip和端口号

方法:

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

构造UDP发送的数据报,需要传SocketAddress对象,可以用InetSocketAddress来创建这个对象

InetSocketAddress:

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

实现一个最简单的UDP客户端服务器程序,不涉及业务流程用于演示api做法,

回显服务器(echo server):客户端发什么请求,服务器就返回什么响应,没有任何业务逻辑

服务端代码:

java 复制代码
public class udpEchoServer {
    private DatagramSocket socket = null;

    public udpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);//对于服务器一端来说,创建socket对象需要指定端口号作为参数,后续服务器运作之后,操作系统会把端口号和进程关联起来
        //调用构造方法的过程中,JVM会调用系统的socket api,完成端口号-进程 之间的关联关系,这样的操作也叫绑定端口号
        //对于一个系统来说,同一个协议下,同一时刻一个端口号只能被一个进程绑定,但是一个进程可以绑定多个端口号(通过创建多个socket对象来完成),端口号就是为了区分进程
    }

    //启动服务器,服务器的工作是处理客户端发来的请求

    //请求(request):客户端主动给服务器发起的数据
    //响应(response):服务器给客户端返回的数据
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            //处理请求

            //服务器一般分为3个步骤
            //1.从网卡中读取客户端的请并且解析求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //receive的参数是DatagramPacket对象,需要构造一个DatagramPacket对象,而构造DatagramPacket对象需要传入一个字节数组和数组长度,长度要保证能够存储数据包
            //从网卡收到的数据(是二进制数据)也会写入到数组中
            //如果网卡上收到数据,receive立即返回,并且获取到数据
            //如果网卡上没有数据,receive会阻塞等待,直到收到了数据为止
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());//因为收到的数据是二进制的,把数据转换成字符串,更好观察

            //2.根据请求计算响应,此处是回显服务器,响应就是请求
            String response = process(request);

            //3.把响应写回到客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length
                    , requestPacket.getSocketAddress());
            //第一个参数是获取字节数组(因为创建String对象的时候就是使用数组的方式),
            //第二个参数是这个数组的长度,不是字符串的长度!!
            //第三个参数是客户端的信息(IP+端口号),因为UDP是无连接的,不保存双方的信息,在进行send时我们要自己把这些信息写进数据包中
            socket.send(responsePacket);

            //4.打印日志
            System.out.printf("[%s:%d] req=%s,resp=%s\n", requestPacket.getAddress(), requestPacket.getPort(),
                    request, response);
        }
    }

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

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

客户端代码:

java 复制代码
public class udpEchoClient {
    DatagramSocket socket = null;

    private String serverIp;//服务器的ip
    private int serverPort;//服务器的端口号

    public udpEchoClient(String serverIp, int serverPort) throws SocketException {
        socket = new DatagramSocket();
        //客户端一般不指定端口号,客户端是主动的一方,不需要让服务器找他.让系统自动分配端口
        //用户的电脑上运行了很多程序,用户可能指定的端口号是和其他程序的端口冲突了
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        System.out.println("启动客户端");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            //1.从控制台读取到用户的输入
            String request = scanner.nextLine();
            //2.构造UDP请求,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length
                    , InetAddress.getByName(this.serverIp), this.serverPort);//
            //InetAddress.getByName(this.serverIp),这里是把ip包裹一下,转换成所需的二进制格式,
            socket.send(requestPacket);
            //3.从服务器中读取响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //4.把响应打印到看看系统上
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

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

服务器的基本流程:

1、读取请求并解析

2、根据请求计算响应

3、响应写回客户端

客户端的基本流程:

1、从控制台读取数据

2、构造请求,发送给服务器

3、从服务器读取响应

4、显示结果到控制台

在Windows上使用如下命令,可以查看主机上的网络相关信息

cmd 复制代码
netstat -ano

使用如下命令,可以查找指定端口号的网络信息

cmd 复制代码
netstat -ano findstr 端口号

TCP的api

核心的两个类:ServerSocket、Socket,ServerSocket是专门给服务器使用的socket对象;Socket既会给客户端使用,也会给服务器使用

TCP是面向字节流的,传输数据的基本单位就是字节byte

ServerSocket的构造方法:

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

ServerSocket的方法:

建立连接的过程类似打电话,accept相当于接电话

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

Socket的构造方法:

构造Socket对象,就是和服务器打电话,建立连接

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

Socket的方法:

方法 说明
InetAddress getInetAddress() 返回套接字所连接的地址(得到对端的信息)
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

实现一个回响服务器:

服务器端:

java 复制代码
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("启动服务器");
        while (true) {
            Socket clientSocket = serverSocket.accept();//接电话,
            //Socket对象用于和客户一对一服务,ServerSocket用于揽客
            Thread t = new Thread(()->{
                try {
                    processConnection(clientSocket);//针对一个连接,使用多线程来提供处理逻辑
//processConnection代码中也有一个while循环,进入里层循环就不能执行外层循环了,此时当有多个客户端发来请求,只能处理一个,使用多线程解决这个问题
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            //这里的Socket对象:clientSocket是连接级别的数据,需要主动close
            // 客户端断开连接之和就要close
            t.start();
//			使用线程池
//			ExecutorService service = Executors.newCachedThreadPool();
//          service.submit(()->{
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
        }
    }

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                //1.读取请求并且解析
                if (!scanner.hasNext()) {
                    //scanner中无法读取数据,说明客户端关闭了连接,导致服务器端读取到末尾
                    break;
                }
                //2.根据请求计算响应
                String request = scanner.next();
                String response = process(request);
                //3.把响应写回客户端
//                outputStream.write(response.getBytes());
                printWriter.println(response);
                printWriter.flush();//刷新缓冲区
                //4.打印日志
                System.out.printf("[%s:%d] req=%s;rep=%s\n", clientSocket.getInetAddress(), clientSocket.getPort()
                        , request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
        System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
    }

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

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

客户端:

java 复制代码
public class TCPEchoClient {
    private Socket socket = null;

    public TCPEchoClient(String serverIP, int ServerPort) throws IOException {
        socket = new Socket(serverIP, ServerPort);
    }

    public void start() {
        System.out.println("客户端启动");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            Scanner in = new Scanner(System.in);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                //1.从控制台读取
                System.out.println("->");
                String request = in.next();
                //2.把请求发送给服务器
                printWriter.println(request);
                printWriter.flush();
                //printWriter这样的类,自带缓冲区,写入数据不会立即触发IO,而是先放入缓冲区中,等缓冲区攒了一波之和,再统一性发送
                //使用flush可以主动刷新缓冲区
                //3.从服务器读取响应
                if (!scanner.hasNext()) {
                    break;
                }
                String response = scanner.next();
                //4.打印结果
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

短时间内有大量客户:

1、客户端发送完一个请求之后很快就断开连接了:使用线程池

2、客户端持续的发送请求处理响应,连接会保持很久:IO多路复用

长连接和短连接:

长连接:客户端连上服务器之后,一个连接中会发起多次请求,接受多个响应

短连接:客户端连上服务器之后,一个连接中会发起一次请求,接受一个响应

相关推荐
车载诊断技术4 分钟前
电子电气架构 --- 什么是EPS?
网络·人工智能·安全·架构·汽车·需求分析
武子康5 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
KevinRay_8 分钟前
Python超能力:高级技巧让你的代码飞起来
网络·人工智能·python·lambda表达式·列表推导式·python高级技巧
豪宇刘1 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
2301_819287121 小时前
ce第六次作业
linux·运维·服务器·网络
秋恬意1 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
CIb0la1 小时前
GitLab 停止为中国区用户提供 GitLab.com 账号服务
运维·网络·程序人生
Black_mario1 小时前
链原生 Web3 AI 网络 Chainbase 推出 AVS 主网, 拓展 EigenLayer AVS 应用场景
网络·人工智能·web3
FF在路上2 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html