Socket套接字(实现TCP和UDP的通信)

目录

一、Socket概述

1.1 Socket通信有两种方式:TCP和UDP

1.2 TCP和UDP的区别

二、UDP数据报套接字编程

三、TCP流套接字编程

四、TCP和UDP缓冲区

4.1 TCP缓冲区

4.2 UDP缓冲区


一、Socket概述

Socket 套接字,是由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元。基于Socket 套接字的网络程序开发就是网络编程。换句话说, 一个Socket由一个IP地址和一个端口号唯一确定 ,利用Socket能比较方便的实现两端的网络通信。

1.1 Socket通信有两种方式:TCP和UDP

TCP通信:客户端提供了 java.net.Socket类,服务器端提供了 java.net.ServerSocket类。
UDP通信:UDP通信不建立逻辑连接,使用 DatagramPacket类打包数据包,使用 DatagramSocket类发送数据包。

1.2 TCP和UDP的区别

· TCP是面向连接的;UDP是无连接的,即发送数据之前不需要建立联系。
· TCP提供可靠的传输。也就是说,通过TCP连接传达的数据,无差错,不丢失,不重复,且按序送达;UDP尽最大努力交付,即不保证可靠交付。
· TCP面相字节流;UDP面向数据报。UDP只能一次接收/发送一个完整数据报和多个数据报,不能是半个数据报。
· 两者都是全双工
解释一下什么是全双工

类似于两辆车在高速公路行驶,中间是一个隔离带,车辆可以既可以从这个方向走,也可以沿着那条方向上走,支持双向通信。

Socket通信模型如下图:

 Socket的发送和接受原理剖析

当创建一个TCP socket对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存中的一片空地。

发送原理

send发送数据,必须得通过网卡发送数据,应用程序是无法通过网卡发送数据的,它需要调用操作系统接口,也就是说,应用程序把发送的数据先写入发送缓冲区(内存中的一块空地),再由操作系统控制网卡把发送缓冲区的数据发送给服务器网卡。

接收原理

应用程序无法直接从网卡接收数据的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区,应用程序再从接收缓冲区获取客户端发送的数据。

二、UDP数据报套接字编程

DatagramSocket API

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

DatagramSocket 构造方法

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

 这里简单介绍下构造方法

不带参数的构造方法一般用于客户端,举一个简单的例子,比如去食堂吃饭,到一个店铺去点餐,在老板还没弄好之前我会随机挑选一个位置,如果这个座位被占用了,我会选下一个座位,所以客户端一般不带有端口号;带参数的构造方法一般用于服务器,同理去食堂吃饭,我和老板提前商量会去他那吃饭,它需要给我一个详细的地址,这个地址就相当于端口号,以至于我到了不会去错地方,所以服务器一般带有端口号。

DatagramSocket 方法

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

 DatagramPacket API

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()
获取数据报中的数据
InetSocketAddress API
InetSocketAddress SocketAddress 的子类 )构造方法:
方法方法说明
InetSocketAddress(InetAddress addr, int port)
创建一个 Socket 地址,包含 IP 地址和端口号

2.1 实例

使用UDP版的回响服务器进行通信

服务端

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


public class UdpEchoServer {
    //需要先定义一个socket对象
    //通过网络通信,必须要使用socket对象
    private DatagramSocket socket = null;

    //为什么需要抛一个异常呢? 因为绑定一个端口不一定能成功
    //如果某个端口已经被别的进程占用了,此时这里绑定操作就会出错
    //同一个主机上,一个端口,同一时刻只能被一个进程绑定
    public UdpEchoServer(int port) throws SocketException {
        //构造socket的同时,指定要关联/绑定的端口
        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
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //2、根据请求计算响应(此处省略这个步骤)
            String response = process(request);
            //3、把响应结果写回到客户端
            //根据response字符串,构造一个DatagramPacket
            //和请求packet不同,此处构造响应的时候,需要指定这个包要发给谁
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    //requestPacket是从客户端这里收来的,getSocketAddress就会得到客户端的ip和端口
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }

    //这个方法希望是根据请求计算响应
    //这里写的是一个回响程序,请求是啥,响应就是啥
    //如果后续写个别的服务器,不需要回响,而是有具体的业务了,就可以修改process方法
    //根据需要来重新构造响应
    public String process(String request) {
        return request;
    }

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

}

 这里讲解下这部分

因为receive方法是Java中的输出型参数,类似于这里我先使用DatagramPacket构造一个空饭盒,receive里面的参数就相当于食堂阿姨接收到了这个空饭盒,再给你这个空饭盒盛菜,也可以这么说,DatagramSocket相当于外卖员,DatagramPacket相当于外卖。

客户端

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 serverIP, int serverPort) throws SocketException {
        //对于客户端来说,不需要显示关联端口
        //不代表没有端口,而是系统自动分配了个空闲的端口
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        //通过这个客户端可以多次和服务器进行交互
        Scanner scanner = new Scanner(System.in);
        while (true) {
            //1、先从控制台读取一个字符串过来
            System.out.print("-> ");
            String request = scanner.next();
            //2、把字符串构造成UDP packet,并进行发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIP), serverPort);
            socket.send(requestPacket);
            //3、客户端尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //4、把响应数据转换成String显示出来
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.printf("req: %s, resp: %s\n", request, response);
        }
    }

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

程序整个流程介绍:

1、服务器先启动,创建DatagramSocket对象,监听端口,用于接收

2、服务器端创建DatagramPacket对象,打包用于接收的数据包

3、服务器阻塞等待接收

4、客户端启动,创建DatagramSocket对象,监听端口,用于接收

5、客户端创建DatagramPacket对象,打包用于发送的数据包

6、客户端发送数据,服务端接收

7、服务端接收数据后,创建DatagramPacket对象,打包用于发送的数据包,发送数据

8、客户端创建DatagramPacket对象,打包用于接收的数据包,阻塞等待接收

9、客户端接收服务端数据,断开连接,释放资源

三、TCP流套接字编程

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

ServerSocket方法

方法方法说明
Socket accept()
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket 对象,并基于该Socket 建立与客户端的连接,否则阻塞等待
void close()
关闭此套接字
Socket API
Socket 是客户端 Socket ,或服务端中接收到客户端建立连接( accept 方法)的请求后,返回的服务端 Socket。
Socket 构造方法:
方法方法说明
Socket(String host, int port)
创建一个客户端流套接字 Socket ,并与对应 IP 的主机上,对应端口的
进程建立连接

Socket方法:

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

3.1 实例

服务器端

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 {
        ExecutorService executorService = Executors.newCachedThreadPool();
        System.out.println("启动服务器!");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            //如果直接调用,该方法会影响到这个循环的二次执行,导致accept不及时了
            //创建新的线程,用新线程来调用processConnection
            //每次来一个新的客户端都搞一个新的线程即可
//            Thread thread = new Thread(() -> {
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            thread.start();
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    //通过这个方法来处理一个连接
    //读取请求
    //根据请求计算响应
    //把响应返回给客户端
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        //try()这种写法,()里面允许写多个流对象,使用;来分割
        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()) {
                    //读取的流到结尾了(对端关闭了/客户端关闭连接)
                    System.out.printf("[%s:%d] 客户端下线\n!", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                //直接使用scanner读取一段字符串
                //next操作读出的结果是不带换行的
                String request = scanner.next();
                //2、根据请求计算响应
                String response = process(request);
                //3、将响应写回给客户端,不要忘了响应里面也是要带换行的
                //返回响应要把换行加回来,方便客户端那边区分从哪里到哪里是一个完整响应
                printWriter.println(response);
                System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }

    }

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

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

 

在服务器端我们使用了try-with-resources语句,确保在代码块执行完毕后自动关闭资源,无论代码执行过程中是否发生异常。

当程序执行到try语句块结束时,如果resource实现了AutoCloseable或Closeable接口,那么close()方法将被自动调用。同时在服务器端引入了多线程的写法,保证服务器能连接多个客户端,同时与多个客户端保持通信,具体实现逻辑如下:

服务器的主线程(main 线程)负责运行 while 循环,用于接收客户端的连接请求。
与此同时,针对每个接收到的连接请求,都会创建一个新线程处理与该客户端的数据通信。这些新线程与主线程是并发执行的。由于主线程和新创建的线程并发执行,服务器可以在处理一个客户端连接的同时,继续接收其他客户端的连接请求。 这使得服务器可以并发处理多个客户端连接,提高了服务器的处理能力。

当然我们也可以引入线程池来优化这段代码:

线程池中的线程数量是动态调整的。
当有新任务提交时,如果线程池中有空闲线程,那么会复用空闲线程来执行新任务;如果没有空闲线程,则会创建一个新线程来执行新任务。
当线程池中的线程空闲时间超过一定时间(默认为 60 秒)时,线程池会回收这个空闲线程。
好处:线程池可以复用已经创建的线程,避免了频繁地创建和销毁线程所带来的性能开销。当有新任务到来时,线程池会优先使用空闲的线程,从而提高系统资源的利用率。

客户端

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 port) throws IOException {
        //这个操作就相当于客户端和服务器建立tcp连接
        //这里连接连上了,服务器的accept就会返回
        socket = new Socket(serverIp, port);
    }

    public void start() {
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){
            //outputStream是一个字节输出流,通过printWriter对象的构造方法将其包装成字符输出流,以便能够更方便地写入字符数据
            PrintWriter printWriter = new PrintWriter(outputStream);
            //scannerFromSocket对象是将输入流对象包装成一个scanner对象,以便能够方便地读取输入流中的数据
            //scanner对象会自动解析和分隔输入流中的数据,并将其转换为相应的数据类型或字符串
            Scanner scannerFromSocket = new Scanner(inputStream);
            while (true) {
                //1、从键盘上读取用户输入的内容
                System.out.print("->");
                String request = scanner.next();
                //2、把读取的内容构造成请求发送给服务器
                //注意,这里的发送是带有换行的
                printWriter.println(request);
                printWriter.flush();
                //3、从服务器读取响应内容
                String response = scannerFromSocket.next();
                //4、把响应的结果显示到控制台上
                System.out.printf("req: %s; resp: %s\n", request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

PrintWriter printWriter = new PrintWriter(outputStream);           

outputStream 是一个字节输出流,通过 PrintWriter 对象的构造方法将其包装成字符输出流,以便能够方便地写入字符数据。

Scanner scannerFromSocket = new Scanner(inputStream);

scannerFromSocket 对象是将输入流对象包装成一个 Scanner 对象,以便能够方便地读取输入流中的数据。
Scanner 对象会自动解析和分隔输入流中的数据,并将其转换为相应的数据类型或字符串。

 程序整个流程介绍:

1、服务端先启动,创建ServerSocket对象,等待连接。

2、客户端启动,创建Socket对象,请求连接。

3、服务器端接收请求,调用accept方法,并返回一个Socket对象,连接成功

4、客户端的Socket对象通过调用getOutputStream()方法获取OutputStream对象,并使用write()方法将数据写入到发送缓冲区。随后,通过调用flush()方法确保数据已被发送出去

5、服务器端Socket对象通过调用getInputStream()方法获取与该socket关联的InputStream实例,然后使用read()方法从接收缓冲区中读取数据

6、客户端释放资源,断开连接。

四、TCP和UDP缓冲区

4.1 TCP缓冲区

创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用write时,数据会先写入发送缓冲区中;
如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适
的时机发送出去;
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面,TCP的另一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既
可以读数据,也可以写数据。这个概念叫做全双工。

4.2 UDP缓冲区

UDP只有接收缓冲区,没有发送缓冲区
UDP没有真正意义上的发送缓冲区。发送的数据会直接交给内核,由内核将数据传给网络层协议
进行后续的传输动作;
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一
致;如果缓冲区满了,再到达的UDP数据就会被丢弃;

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值