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

🥥 Socket API

网络编程嵌套字就是操作系统给应用程序提供的一组API,嵌套字是通信的基石

socket 译为插座,也可以认为是应用层和传输层之间的桥梁,socket是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。socket允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。

在传输层中有两组重要的协议 TCP/UDP,对于这两个协议,socket API也有对应的两组,这两组API是传输层和应用层传输数据的重要接口

🍑 TCP/UDP

TCP/UDP虽然都是传输中的核心协议,但是他们之间有很大的区别

我们列一个表格来分析 🌰

TCPUDP
有连接无连接
可靠传输不可靠传输
面向字节流面向数据报
全双工全双工
  • 有连接和无连接

有连接就类似于打电话☎️ ,得有人接通,才能把消息传到,无连接就像是微信,不需要接通消息也能传达到

  • 可靠传输和不可靠传输

数据传到后,发送方知道收件方是否收到了信息,这是可靠的,否则是不可靠的,举个🌰 打电话,就是可靠的因为对方没接发送发就知道没收到消息,接了就说明对方收到了消息,已读就是可靠传输,但是发微信这种就是不可靠的,我们无法知道对方是否收到了消息

注意⚠️ 可靠传输的错误理解:可靠传输就是数据传输过去后,对方百分百能接收到可靠传输就是安全传输这是不正确的

  • 面向字节流和面向数据报

面向字节流:数据以字节为单位传输(类似于文件操作中的字节流)
面向数据报:以数据报为单位进行传输(一个数据报都会明确大小),一次发送和接收必须是一个完整的数据报,不能是半个也不能是一个半,必须是完整的

  • 全双工和半双工

这两个协议都是全双工的,代表数据之间的通信是双向的,一条链路,双向通信,代表传输层可以发数据也可以接受数据,反而言之,半双工就带=代表一个链路,单向通信

接下来我们分别用UDP和TCP的嵌套字来实现简单的服务端和客户端的交互

🍉UDP Socket

UDP Socket主要包含了两个重要的类DatagramSocketDataPacket

DatagramSocket:主要包含了两个个方法:receive和send方法,但是需要注意这个DatagramSocket对象对应到操作系统的一个socket文件
在之前,我们介绍到过操作系统中的文件,这是一个广义的概念,实际上操作系统中的文件还可能包含硬件设备和一些软件资源,网卡就是这种典型的硬件设备,socket就是对应网卡这种硬件设备
> 从socket文件读数据就是从网卡中读取数据
往socket文件写数据就是往网卡中写入数据

可以把socket文件比作一个操作网卡的遥控器,通过socket文件来操作网卡中的数据

DatagramPacket:并不像socket那么复杂,DatagramPacket对象就是代表数据报的意思,即使用UDP传输数据的基本单位,因为UDP是面向数据报的方式传输的,所以需要对数据进行封装,将数据变为数据报

我们根据以上这两个类实现一个简单的回响服务器和客户端来帮助我们理解UDP协议的特点和使用

🍉 服务器端

  • 创建一个服务器端的类,并且创造一个实例
public class UdpEchoServer {
    //进行网络编程,第一步就是需要准备好socket实例,这是进行网络编程的大前提
        private DatagramSocket datagramSocket = null;
        }
  1. 使用构造方法来创建一个端口号
 public UdpEchoServer (int port) throws SocketException {
            datagramSocket = new DatagramSocket(port);
        }

这里我们做详细说明,在创造服务器端的时候需要指定一个明确的端口号,构造服务器端和客户端的时候都需要指定一个端口号 但是服务器是必须的,客户端不是必须的,因为服务器端需要客户端来访问,服务器不指定一个固定的端口号的话,客户端就没办法访问,就好比开了一个饭店🏨 ,但是不告诉顾客们🏨 在哪,那就不会有客人来了,但是客户端不同,客户端不用一定非得指定,这不代表客户端就没有端口号了,无论是客户端还是服务端端口号都一定有的,这是硬性的,而且每个端口号都不同,但是客户端的端口号系统会自动分配,这样就避免了因为端口号冲突而产生异常

  1. 启动服务器
    服务器的启动需要以下几个步骤
 public void start() throws IOException {
            System.out.println("启动服务器!");
            //Udp不需要建立链接,直接收客户端发来的请求即可
            while(true){
                //1.读取客户端发来的请求
                DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//->构造了一个空间
                datagramSocket.receive(requestPacket);//为了接收数据需要准备什么一个空的DatagramPacket,这是一个输出型参数
                //把DatagramPacket解析成一个String
                String request = new String(requestPacket.getData(),0, requestPacket.getLength(),"UTF-8");
                //2.根据请求计算响应(此处是回显服务,省略
                String response = process(request);
                //3.把响应写回到客户端
                DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                        requestPacket.getSocketAddress());
                datagramSocket.send(responsePacket);//我们需要知道服务端发给谁就需要知道IP和端口
                System.out.println();
                System.out.printf("[%s:%d] req : %s,resp:%s\n",requestPacket.getAddress().toString(),
                        requestPacket.getPort(),request,response);
            }
        }
  1. 使用receive来接受客户端传来的请求,但是我们发现receive中的参数是DatagramPacket的所以就需要对这个请求进行封装,然后再将这个请求解析成String类型
  2. 根据请求计算响应,我们这只是一个简单的回响服务器,所以就创建一个简单的process方法返回响应即可
  3. 把响应写回到客户端,我们需要用到send这个核心的方法,此方法的参数和receive的参数类似,也是DatagramPacket类型的参数,但是send需要知道客户端的IP和端口号所以又加了一个参数requestPacket.getSocketAddress()),这里面包含了IP地址和端口号

最后在主函数中创造UdpEchoServer类的实例

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

服务器端的代码完整如下

//Udp服务端
public class UdpEchoServer {
    //进行网络编程,第一步就是需要准备好socket实例,这是进行网络编程的大前提
        private DatagramSocket datagramSocket = null;

        public UdpEchoServer (int port) throws SocketException {
            datagramSocket = new DatagramSocket(port);
        }
        //启动服务器
        public void start() throws IOException {
            System.out.println("启动服务器!");
            //Udp不需要建立链接,直接收客户端发来的请求即可
            while(true){
                //1.读取客户端发来的请求
                DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//->构造了一个空间
                datagramSocket.receive(requestPacket);//为了接收数据需要准备什么一个空的DatagramPacket,这是一个输出型参数
                //把DatagramPacket解析成一个String
                String request = new String(requestPacket.getData(),0, requestPacket.getLength(),"UTF-8");
                //2.根据请求计算响应(此处是回显服务,省略
                String response = process(request);
                //3.把响应写回到客户端
                DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                        requestPacket.getSocketAddress());
                datagramSocket.send(responsePacket);//我们需要知道服务端发给谁就需要知道IP和端口
                System.out.println();
                System.out.printf("[%s:%d] req : %s,resp:%s\n",requestPacket.getAddress().toString(),
                        requestPacket.getPort(),request,response);
            }
        }
        public String process(String request){
            return request;
        }

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

}

服务器端需要和客户端一起使用才能实现功能,下面我们创建一个回响客户端

🍉 客户端

1.与服务器端相同,也需要构造一个客户端的类,并使用DatagramSocket来创造一个实例,但是不同的时客户端的构造方法不需要指定端口号

public class UdpEchoClient {
    DatagramSocket socket = null;
    public UdpEchoClient(String Ip,int port) throws SocketException {
        socket = new DatagramSocket();
    }

  1. 启动客户端
public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while(true){
            //1.先从控制台读取用户输入的字符串
            System.out.print("-> ");
            String request = scanner.next();
            //2.把这个用户输入的内容构成一个UDP请求,并发送
              //这里面包含两部分内容
              //1)将读取的字符串变为DatagramPacket请求
              //2)数据要发给谁,即需要知道服务器的IP和端口
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName("127.0.0.1"),9090);
            socket.send(requestPacket);
            //3.从服务器读取相应数据,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,requestPacket.getLength(),"UTF-8");
            //4.把响应结果显示到控制台上
            System.out.printf("req: %s,resp:%s\n",request,response);
        }
    }

我们重点解析第二个方块,这段内容我们会发现与服务器端不同,这里面端口号和IP地址是分开的,其实这两种都是用于构造方法,技能构造数据,又能构造目标地址,只不过一个是合起来的,一个是分开的,分开的更好理解,在客户段中我们,需要知道次处是代表服务端的端口号和IP地址,直接在这里指定,但也可以在构造方法里面指定,在构造方法中加入两个参数,一个是端口号一个是IP地址,只不过这不是代表客户端的,而是表示服务器的,具体代码如下

DatagramSocket socket = null;
    private String serveIp;
    private int servePort;
    public UdpEchoClient(String Ip,int port) throws SocketException {
        this.serveIp = Ip;
        this.servePort = port;
        socket = new DatagramSocket();
    }

然后在主函数中构造实例,整体如下

public class UdpEchoClient {
    DatagramSocket socket = null;
    private String serveIp;
    private int servePort;
    public UdpEchoClient(String Ip,int port) throws SocketException {
        this.serveIp = Ip;
        this.servePort = port;
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while(true){
            //1.先从控制台读取用户输入的字符串
            System.out.print("-> ");
            String request = scanner.next();
            //2.把这个用户输入的内容构成一个UDP请求,并发送
              //这里面包含两部分内容
              //1)将读取的字符串变为DatagramPacket请求
              //2)数据要发给谁,即需要知道服务器的IP和端口
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serveIp),servePort);
            socket.send(requestPacket);
            //3.从服务器读取相应数据,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,requestPacket.getLength(),"UTF-8");
            //4.把响应结果显示到控制台上
            System.out.printf("req: %s,resp:%s\n",request,response);
        }
    }

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

这样一段完整的客户端和服务器的代码就完成了

在运行时,我们知道一个服务器按理来说可以连接许多不同的客户端,可是如果你真的运行了,会发现只能连接一个客户端,这是因为IDEA的默认设置,我们只需要更改一下设置即可

请添加图片描述
请添加图片描述
勾选蓝色方块即可
请添加图片描述
这样一段完整的客户端和服务器就构建好了,这是基本的框架,如果在业务上有更多的需求的话只需要更改服务代码中的process()方法即可

我们可以创造一个新类继承回响服务器的代码,然后重写process方法,完成业务上的逻辑即可,比如我们实现字典翻译的逻辑就可以这样写代码

public class UdpDictServer extends UdpEchoServer{
    private HashMap<String,String> hashMap = new HashMap<>();
    public UdpDictServer(int port) throws SocketException {
        super(port);
        hashMap.put("cat", "小猫");
        hashMap.put("dog","小狗");
        hashMap.put("fuck","卧槽");
        hashMap.put("monkey","猴子");
    }

    @Override
    public String process(String request) {
        return hashMap.getOrDefault(request,"该词无法翻译");
    }

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

🍎 五元组的概念

在写上述相关代码的时候,我们不难发现我们会频繁的和IP,端口号打交道
这是因为一次通信是由五个核心信息描述出来的,分别是原IP,原端口,目的IP,目的端口,协议类型

站在不同的角度上,这五元组代表的含义是不同的,拿上面的服务器和客户端为例

  • 服务器
原IP服务器程序本机的IP
原端口服务器绑定的端口
目的IP客户端的IP(包含在收到的数据报中)
目的端口客户端的端口(包含在收到的数据报中)
协议类型UDP
  • 客户端
原IP本机的IP
原端口系统分配的端口
目的IP服务器的IP
目的端口服务器的端口
协议类型UDP

我们会发现站在不同的角度上五元组除了协议类型一样其他的都是不一样的,TCP的五元组也和这个相同,只不过是协议类型不一样

🥭 TCP Socket

TCP socket与UDP socket截然不同,因为他俩的特性不同,逻辑也不同,所使用的类也不相同

首先类的使用就不相同,TCP socket api 中也是包含了两个类ServerSocket和Socket,ServerSocket是专门给服务器用的,Socket即是给服务器的也是给客户端的

这里面Socket在TCP中我们会经常使用到,他有许多方法,我们在代码中解析,但是需要非常注意的是ServerSocket,他有一个特别重要的方法就是accpet()方法这是TCP中连接的特性,服务器和客户端必须建立连接,才能进行交互,他返回的是一个Socket类型的数据

和UDP构造客户端服务端的整体思路大致相同,只不过TCP是面对字节的,就需要用到我们之前学到的IO流的知识

🥭 服务端

  1. 创建服务端类,创建对象,使用构造方法,需要传入服务器的端口号
public class TcpEchoServer {
    private ServerSocket serverSocket = null;

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

}
  1. 启动服务器
 public void start() throws IOException {
        System.out.println("启动服务器!");
        while(true){
            //1.需要和服务器建立链接,与Udp不同,Tcp是有链接的
            //accept使用的前提是有人给服务端打电话,如果没有客户端建立链接,此处的accept就会堵塞
            //accept返回了一个Socket对象,后续服务端和客户端之间的联系,就是通过这个对象来完成的
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来处理请求和响应
        try(InputStream inputStream = clientSocket.getInputStream()){
            try(OutputStream outputStream = clientSocket.getOutputStream()){
                Scanner scanner = new Scanner(inputStream);
                while(true){
                    //1.读取请求
                    if(!scanner.hasNext()){
                        System.out.printf("[%s:%d] 客户端断开连接\n",clientSocket.getInetAddress().toString(),
                                clientSocket.getPort());
                        break;
                    }
                    String request = scanner.next();
                    //2.根据请求计算响应
                    String response = process(request);
                    //3.把这个反应返回给客户端,为了方便把OutputStream用PrintWriter包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区,如果没有这个客户端可能不能及时看到
                    printWriter.flush();
                    System.out.printf("[%s:%d req : %s,resp :%s\n]",clientSocket.getInetAddress(),clientSocket.getPort(),
                            request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }

1 必须和客户端建立连接,使用accept方法
2.先读数据,将客户端的请求读取,然后计算响应
3.把反映写回给客户端,为了方便把OutputStream用PrintWriter封装一下
4.刷新缓冲区,这样更方便客户能在最短的时间里看到回复
5.释放资源,即clientSocket.close()

我们发现在最后TCP比UDP多了一个释放资源的过程,而且上述代码的Server Socket没有释放资源呢这是为什么呢?
这是因为,UDP中的socket文件和ServerSocket是贯穿整个程序的,最迟也会在进程结束的时候自动释放资源,但是这个client Socket是不一样的,只要断开连接,他就没有什么用了,而且这样的连接有很多,连接断开,也就没什么用了,所以需要释放资源

🥭 客户端

 //用普通的Socket即可,不用ServerSocket了
    //此处不用给客户端提供端口号,系统会自动分配
    private Socket socket = null;
    public TcpEchoClient (String serverIp,int serverPort) throws IOException {
        //此处的参数代表的不是客户端的端口号代表的是与这个客户端建立连接的服务器的IP地址和端口号
        //调用这个构造方法,就会和服务器建立连接
        socket = new Socket(serverIp,serverPort);
    }
    public void start(){
        System.out.println("和服务器链接成功");
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream()){
            try(OutputStream outputStream = socket.getOutputStream()){
                while(true){
                    //要做的事情,仍然是四步
                    //1.从控制台读取字符串
                    System.out.println("->");
                    String request = scanner.nextLine();
                    //2.根据读取的字符串构造请求,把请求放松给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush();//如果不刷新,服务器无法即使看到数据
                    //3.从服务器中读取响应,并解析
                    Scanner respScanner = new Scanner(inputStream);
                    String response = respScanner.next();
                    //4.把结果显示到控制台上
                    System.out.printf("req : %s,resp : %s\n",request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

和UDP有所不同,UDP是通过数据报的方式传递数据和端口号IP地址,TCP是直接从socket文件中读取的,通过IO流的方式,端口号和IP地址也是直接将服务端放到构造方法里面即可

写完后,我们运行代码会发现一个问题,当我们运行多个客户端的时候,第一个客户端会成功连接,其余的贼会卡住,
请添加图片描述

请添加图片描述
我们发现,客户端与服务器建立连接成功,但是在服务器上没有响应,这是为什么呢?

我们来详细的探讨一下.

我们知道TCP协议需要连接,在代码中会有accept方法来建立连接,在代码中会循环建立连接,连接建成后就会进入processConnection中循环读取数据,又进入了一个循环,也就是说里循环如果不结束的话,外面的循环就会一直在等待,直到里循环结束,外面这个循环,才会重新和新的客户端连接,可以把这个过程当成一个通话过程,只有挂掉当前的电话☎️ ,才能接通下一个电话

解决方案

  1. 关闭上一个客户端,当我们关闭上一个客户端后,下一个客户端才可以和服务器建立连接
    请添加图片描述

但是这种方法不太现实,我们不能说是一个个去关闭线程,这显然是不他太现实的,所以我们使用另一个方法

  1. 使用多线程的思路
    我们可以创建一个线程,这样就有两个线程了,主线程调用accept方法,然后新建的线程,在新的线程里调用process Connection方法,这个时候多个线程是并发执行的关系,宏观上来讲,就是各自执行各的了,这样就避免了循环等待的问题了
while(true){
            //1.需要和服务器建立链接,与Udp不同,Tcp是有链接的
            //accept使用的前提是有人给服务端打电话,如果没有客户端建立链接,此处的accept就会堵塞
            //accept返回了一个Socket对象,后续服务端和客户端之间的联系,就是通过这个对象来完成的
            Socket clientSocket = serverSocket.accept();
            Thread t = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }

同理,如果TCP服务端想要有更多的功能,那就对process进行修改,也就是响应计算,这是服务器程序最复杂的过程

到这里TCP和UDP的所有的基础知识就讲完了,那么随之而来的问题,是否能用UDP的服务器连接TCP的客户端呢?
这显然是不能的!
这就是我们前面讲到的五元组问题,这里面的协议类型必须是相同的,否则就会无法进行网络通信!!!

在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值