首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【javaEE】Socket 编程(UDP/TCP)实战 + 期末大作业

【javaEE】Socket 编程(UDP/TCP)实战 + 期末大作业

作者头像
那我掉的头发算什么
发布2026-01-12 18:21:38
发布2026-01-12 18:21:38
820
举报
这里是@那我掉的头发算什么 刷到我,你的博客算是养成了😁😁😁

网络编程中的基本概念

发送端和接收端 在⼀次⽹络数据传输时: 发送端:数据的发送⽅进程,称为发送端。发送端主机即⽹络通信中的源主机。 接收端:数据的接收⽅进程,称为接收端。接收端主机即⽹络通信中的⽬的主机。 收发端:发送端和接收端两端,也简称为收发端。 注意:发送端和接收端只是相对的,只是⼀次⽹络数据传输产⽣数据流向后的概念

请求和响应 ⼀般来说,获取⼀个⽹络资源,涉及到两次⽹络数据传输: • 第⼀次:请求数据的发送 • 第⼆次:响应数据的发送。

客户端和服务端 服务端:在常⻅的⽹络数据传输场景下,把提供服务的⼀⽅进程,称为服务端,可以提供对外服务。 客户端:获取服务的⼀⽅进程,称为客户端。

Socket套接字

定义

Socket 是操作系统提供的、应用层与传输层之间的编程接口(API),是进程间网络通信的端点抽象,本质是用于跨主机(或本机)进程数据交换的 “通信句柄 / 内核对象”。它屏蔽了 TCP/UDP、IPv4/IPv6 等底层网络协议的细节,为应用程序提供统一的、类似文件 I/O 的操作方式,让开发者无需关注协议底层的握手、分帧、重传等逻辑,只需调用标准接口即可完成网络数据收发。

分类

Socket套接字根据传输层协议划分成三类: 1.流套接字(使用传输层TCP协议) 2.数据报套接字(使用传输层UDP协议) 3.原始套接字(用于自定义传输层协议)

TCP和UDP是两个核心的协议,他们之间差异非常大,所以socket提供了两套不一样的api

TCP:有连接,可靠传输,面向字节流,全双工 UDP:无连接,不可靠传输,面向数据报,全双工

有连接无连接是逻辑上的连接,而非物理上的,实际进行网络通信肯定需要物理上的连接(例如网线相连)。

对于TCP来说,TCP协议中保存了对端的信息,A和B通信,A和B先建立连接,让A保存B的信息,B保存A的信息(彼此之间知道谁是和他连接的那个)–有连接

对于UDP来说,UDP协议本身不保存对方的信息–无连接(当然可以自己用代码设置变量储存对方的信息,但是和UDP无关)

可靠传输/不可靠传输

网络上,数据的传输是非常容易出现丢失的情况(丢包) 光信号和电信号等可能会受到外界影响,信号会发生变化,比如0变1。这样乱了的信号就会被识别出来,把这样的数据给丢弃。

不敢指望,一个数据包发送之后,100%会到达对方。

可靠传输不是保证数据100%到达,而是尽可能的提高传输成功的概率,如果出现丢包了,就能感知到。 不可靠传输只是把数据发了,然后就不管了。

当然可靠传输提高传输率肯定就会付出时间的代价。

面向字节流/面向数据报

面向字节流,读写的时候是以字节为单位的,可以任意长度读取,但会存在粘包问题。 面向数据报,读写数据以一个数据报为单位,有长度限制,但是不存在粘包问题。

全双工/半双工

一个通信链路,支持双向通信(能读也能写)-》全双工 一个通信链路,只支持单向通信(要么读要么写)-》半双工 类似于马路上的单行车道与双向车道

Socket编程

计算机中的“文件”通常是一个广义的概念,文件还可以代指一些硬件设备。所有硬件设备(网卡、硬盘、键盘等)都会被内核抽象成 “设备文件”(属于内核管理的文件对象,不是普通的磁盘文件)。 这么做的目的是统一操作逻辑:无论操作普通文件(如.txt)还是硬件设备,都可以用相同的 “打开(open)→读写(read/write)→关闭(close)” 接口,无需为每种硬件单独设计一套操作方式。

网卡(Network Interface Card)是计算机接入网络的核心硬件设备(也存在虚拟网卡),负责在 “计算机内部数据” 与 “外部网络物理信号” 之间建立连接,主要工作在 OSI 模型的物理层和数据链路层,是计算机联网的 “硬件入口”。

我们在操作网卡的时候,就是将网卡抽象成一个Socket文件(将网卡抽象成一个靠Socket来操作的设备文件),操作流程和操作普通文件差不多,打开-读写-关闭。这个Socket相当于网卡的遥控器。

下面提到的Socket实际上就是网卡的一系列操作

UDP编程

UDP的Socket api DatagramSocket 构造⽅法:

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

DatagramSocket ⽅法:

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

DatagramPacket DatagramPacket是UDP Socket发送和接收的数据报。 DatagramPacket 构造⽅法:

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

DatagramPacket ⽅法:

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

下面,其实我们就可以尝试“手搓”一个简单的客户端服务器端交互了:

第一个服务器我们可以做的简单一点,将处理请求的操作简化,使其变成一个回显服务器,回显的意思就是你发什么他就返回什么。话不多说,直接展示:

服务器:

代码语言:javascript
复制
package NetWork;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

public class UdpEchoServer {
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动了");
        while(true){
            //1.获取客户端请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(requestPacket);
            //2.处理客户端请求:回显服务器直接返回原数据
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            String response = process(request);
            //3.返回响应
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
                    response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印日志
            System.out.printf("[%s:%d] req : %s resp : %s \n",requestPacket.getAddress(),requestPacket.getPort(),request,response);

        }
    }

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

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

客户端:

代码语言:javascript
复制
package NetWork;

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

public class TcpEchoClient {
    Socket socket = null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                //1.获取用户输入
                String request = scanner.next();
                //2.将请求发送出去
                writer.println(request);
                writer.flush();
                //3.获取到服务器的响应
                String response = scannerNet.next();
                //4.打印
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

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

我们来分段式讲解每一部分的内容:

首先是服务器端:

为何死循环?
在这里插入图片描述
在这里插入图片描述

服务器启动之后,我们不清楚客户端会有多少的请求,所以我们设置一个死循环,不断地读取客户端的请求。

获取客户端请求
在这里插入图片描述
在这里插入图片描述

udp协议中数据的传输是以数据报为单位的,我们获取时先自己构建一个数据报,接下来是receive方法,receive方法没有返回值,没法通过返回值得到请求,所以这个receive方法依然是使用了输出型参数的思想,将请求传递给了requestPacket,输出型参数详解见文件内容读取的那篇博客: 博客链接::文件内容操作

处理客户端请求
在这里插入图片描述
在这里插入图片描述

因为传输过来的数据是二进制的,我们需要先将数据进行解析。此处用到了String的一个构造方法,传入的参数第一个是字节数组,可以使用封装好的方法getData直接获取,其次是起始位置以及数据长度。注意此处的数据长度应该是原二进制数据的数组长度而非字符串的长度。

返回响应
在这里插入图片描述
在这里插入图片描述

返回响应时,为了更好的传输,还是要打一个数据报,这个数据报的构造方法的参数包括1.字节数组,这个直接使用getBytes方法就可以将字符串转换了,2.长度,还是要使用字节数组的长度而非字符串长度。3.要想传递信息,就要知道传递的目的是哪儿。第三个参数getSocketAddress()方法其实会返回两个值,一个是requestPacket(也就是用户传递过来的数据报)的IP,一个是PORT,知道了IP和端口就能精准将响应传递给用户。

客户端

发送请求
在这里插入图片描述
在这里插入图片描述

发送请求时依旧先将字符串的内容转换成字节流,构造一个包,此处传目的ip和端口的时候采用的是另一种传参方法,因为此时我们是知道目标服务器的ip和地址,所以就要分别传参。此处端口可以直接传,但是传ip时需要用getByName方法再转换一下,因为此处java封装的时候没有提供直接传参的方法。

接受请求
在这里插入图片描述
在这里插入图片描述

我们传输请求之后,服务器端会产生响应最终被用户端接收。此处接收时依旧是receive方法使用输出型参数。

客户端和服务器端构造方法传参为何不一致
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

服务器端构造网卡时指定端口号,客户端不指定端口号

因为,客户端在访问服务器时,必须先知道服务器的ip和端口号才可以访问的到,否则数据不知道朝着哪里传输。而客户端的网卡的端口代表的是客户端在运行程序时在哪个端口运行(端口唯一标识一个进程,两个进程不可以同时使用一个端口),实际运行中为了避免冲突,端口号应该是随机分配一个空闲端口。而如果服务器端口真的冲突了,因为服务器是在程序员手里的,可以很方便的修改,并且修改一次就行。如果用户自己的冲突了,不可能让程序员把所有用户的端口号都改了。

环回ip
在这里插入图片描述
在这里插入图片描述

我们在指定服务器ip时,使用的是127.0.0.1。这是一个特殊的ip,叫做环回ip,无论你主机的ip是什么,都可以使用127.0.0.1代替。也就是说,如果在同一台主机上,127.0.0.1永远代表当前主机,相当于“this”。

有业务能力的服务器

像我们上面写的回显服务器,其实是没啥意义的,她不能解决任何实际问题。我们可以扩展一下服务器业务逻辑,让他能实现一些类似于翻译的小功能

代码语言:javascript
复制
package NetWork;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

public class UdpDictServer extends UdpEchoServer{

    HashMap<String,String> dict = new HashMap<>();
    public UdpDictServer(int port) throws SocketException {
        super(port);
        dict.put("小狗","dog");
        dict.put("小猫","cat");
        dict.put("小猪","pig");
        dict.put("小鸭子","duck");
    }

    public String process(String request){
        return dict.getOrDefault(request, "未找到该词条");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9090);
        server.start();
    }
}
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

虽然业务逻辑很简单,但好歹也可以解决一点点一点点的问题哈。

TCP编程

API: ServerSocket:

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

ServerSocket是服务器特有的API,用户端不可以使用。

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

我们知道TCP是有连接的,这里的accept方法本质上就是建立连接,开始监听指定端口(创建时绑定的端⼝),有客户端连接后,返回⼀个服务端Socket对象,并基于该Socket建⽴与客户端的连接,否则阻塞等待。

Socket Socket 是客户端Socket,或服务端中接收到客户端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。 不管是客户端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,及⽤来与对⽅收发数据的。

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

构造方法的参数是服务器ip和端口,用于与服务器直接建立连接。

Socket方法:

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

我们可以直接通过这些方法得到传输的数据,不必像udp里面借助send和receive方法。

OK,学会这些方法的话,相信大家肯定和我一样,迫不及待地想要自己造一个基于TCP协议的服务器端和用户端交互了吧😁:

服务器端:

代码语言:javascript
复制
package NetWork;

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 {
    ServerSocket socket = null;
    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动了");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true){
            // tcp 来说, 需要先处理客户端发来的连接.
            // 通过读写 clientSocket, 和客户端进行通信.
            // 如果没有客户端发起连接, 此时 accept 就会阻塞.
            Socket clientSocket = socket.accept();
            /*Thread thread = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();*/
            service.submit(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });

        }
    }

    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 writer = new PrintWriter(outputStream);
            while(true){
                if(!scanner.hasNext()){
                    System.out.printf("客户端[%s:%d]下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //1.获取请求,可以通过read读取字节流,也可以借助scanner
                String request = scanner.next();
                //2.处理获得响应
                String response = process(request);
                //3.将响应返回
                writer.println(response);
                writer.flush();
                //4.打印日志
                System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(),
                        clientSocket.getPort(),request,response);
            }


        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }
    }

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

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

客户端:

代码语言:javascript
复制
package NetWork;

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

public class TcpEchoClient {
    Socket socket = null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                //1.获取用户输入
                String request = scanner.next();
                //2.将请求发送出去
                writer.println(request);
                writer.flush();
                //3.获取到服务器的响应
                String response = scannerNet.next();
                //4.打印
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

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

注:服务器端的代码是我优化过的,大家先看我下面的讲解之后再来看代码

服务器端

输入流与输出流的配置
在这里插入图片描述
在这里插入图片描述

我们正常熟知的对Scanner进行构造的时候,都是使用System.in作为参数,其实他还有别的构造方法。System.in的构造方法代表输入是通过控制台进行的。如果我们传入的参数为inputStream的话,我们就可以得到流对象里面的输入。

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

此时的scanner.next()返回的就是输入流内部的数据,并且采用scanner的好处是可以直接得到字符类型的输出,不用再进行数据转化了。

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

输出也是一样,可以借助方法直接将字符串传进去,内部jvm封装好的方法会自动将数据进行转化,可以说非常良心!!!

此外,要是有人看不懂try()括号里面定义对象的写法,建议回之前的博客去看try-with-resource用法: 链接地址》直达

请求与响应
在这里插入图片描述
在这里插入图片描述

这样的话,我们可以全程不进行数据转化的情况下,只用一些封装好的方法就可以实现请求的获取以及响应的传输啦!

客户端请求与访问
在这里插入图片描述
在这里插入图片描述

用户端这里也是一样,通过输入流输出流,也可以简单的进行发送请求与获取响应。

拓展:多线程的引入

其实以上的问题解决之后,我们的程序就可以正常运行了。但是,实际运行中如果我们同时创建多个客户端与服务器进行连接,此时会产生一些问题:

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

注意我们这里的逻辑:当一个客户端与服务器建立连接之后,在processConnection这个方法没有结束之前(也就是上一个用户端没有退出之前),第一次循环就不会结束,服务器就无法建立新的连接,后面的所有新的客户端都会被阻塞,无法连接。也就是这个代码无法同时对多个客户端进行服务。

解决办法就是引入多线程(多线程的知识在前面博客中从头到尾系统性的讲过) 1.建立普通线程

代码语言:javascript
复制
Thread thread = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();

每次来了一个新客户端,就建立一个新线程处理任务。 2.建立线程池

代码语言:javascript
复制
ExecutorService service = Executors.newCachedThreadPool();
while(true){
            // tcp 来说, 需要先处理客户端发来的连接.
            // 通过读写 clientSocket, 和客户端进行通信.
            // 如果没有客户端发起连接, 此时 accept 就会阻塞.
            Socket clientSocket = socket.accept();

            service.submit(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });

        }

注意,此处应该使用newCachedThreadPool类似的线程池,不应该用有线程数量限制的线程池,否则程序功能有用户数量的限制。

补充知识:

1.flush():

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

我们都知道,输入输出其实是一个很费时的事情,业务中一个常用的方法是先把输入或输出的数据保存到数据缓冲区内,然后一次性全部输入或输出。此处的PrintWriter就是这样,如果没有flush这行代码,所有的数据都会被放到缓冲区内,不会被实际的输入,服务器端也就没法得到输入,因而不会产生响应,整个程序就坏掉了。

2.真假socket?

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

服务器端建立连接时会得到一个socket对象,用户端也有一个socket对象。这两个对象在不同的进程里,甚至可能在不同的主机上,他俩不是同一个对象。他们俩的关系类似于:两个电话AB,从A说话B能听见,从B说话A能听见,A和B在进行通信,但是他俩不是一个对象。

如何应对计网期末大实验?

如果你和我的学校一样,期末计算机网络实验的大作业也是socket编程的话,如果你恰好上课没咋听,老师又恰好让你给他讲一讲代码,那就有点寄了…

但是!万一你有救呢?

自救指南

1.先问AI,自己的代码是使用udp协议还是tcp协议? 2.定位到服务器和用户端代码里面的start部分和业务逻辑部分 3.以java举例,如果是UDP,找到receive方法,send方法,找到Packet数据报(不用给他讲数据报怎么构造的,你就说你构造了一个数据报,然后指给他就行了)如果是TCP,找找输入流输出流在哪儿,当然有可能你的代码对对象命名不同,你就找InputStream和OutPutStream在哪儿,然后告诉它使用的是字节流进行输入输出。 4.我发现好多人的代码服务器端的ip都是127.0.0.1,记住,这个是环回ip,代表的是当前主机,端口是随便定的。

加分项:(应该不会细问,你就照着说就完了) 服务器为啥要开多线程?(一般在tcp才会涉及) ✅ 话术:“因为 ServerSocket 的 accept ()是阻塞方法,单线程的话只能处理一个客户端,后面的客户端连不上;我给每个客户端开个线程就能同时处理多个用户的消息收发了。”

万金油大招

实在不行,你就背下面这个,直接背就完了,除此以外那我也没招了,喵~~ 老师,我的代码是基于 TCP/UDP 的聊天室,服务器端用 ServerSocket/DatagramSocket 绑定 9999 端口,客户端用 127.0.0.1 连接;核心是用 InputStream/OutputStream(TCP)/DatagramPacket(UDP)做消息收发,多线程处理多客户端连接,实现了群聊 / 私聊、在线用户列表这些核心功能,IP 用环回地址是为了本地测试,端口选 9999 是避开系统端口。

别讲前端!

反正我是不会前端,老师也不会去问前端,前端这一块你就直接掠过就行了…

此外,要是还有什么需要我补充的,欢迎来评论区与我交流!!!!!祝愿大家期末永远不挂科!!!!!!!!!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 这里是@那我掉的头发算什么 刷到我,你的博客算是养成了😁😁😁
  • 网络编程中的基本概念
  • Socket套接字
    • 定义
    • 分类
    • Socket编程
    • UDP编程
      • 为何死循环?
      • 获取客户端请求
      • 处理客户端请求
      • 返回响应
      • 发送请求
      • 接受请求
      • 客户端和服务器端构造方法传参为何不一致
      • 环回ip
      • 有业务能力的服务器
    • TCP编程
      • 输入流与输出流的配置
      • 请求与响应
      • 客户端请求与访问
      • 拓展:多线程的引入
      • 补充知识:
  • 如何应对计网期末大实验?
    • 自救指南
    • 万金油大招
    • 别讲前端!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档