达州市网站建设_网站建设公司_产品经理_seo优化
2025/12/23 18:57:50 网站建设 项目流程

实用指南:网络编程 UDP 和 TCP

网络编程

  • 网络编程的基本概念
    • 什么是网络编程
    • 基本概念
  • Socket套接字
    • 概念
    • 分类
  • UDP数据报套接字编程
    • 回显实例
  • TCP流套接字编程
    • 回显实例
    • 引入多线程
    • 引入线程池
    • 细节处理

网络编程的基本概念

什么是网络编程

在生活中是离不开网络的,我们经常从网络上获取一些资源(像通过浏览器浏览一些视频网站,从里面获取一些视频资源),这个过程都是通过网络编程来实现进行数据传输

网络编程就是网络上的主机,通过不同的进程,进行网络传输数据(以网络编程实现的网络通信)
只要是不同进程就行,因此即使是同一个主机,不同进程也可以进行网络编程
在这里插入图片描述
进程A:获取网络资源
进程B:提供网络资源

基本概念

1.接收端和发送端

发送端:数据的发送方进程,称为发送端,此主机可以称为源主机
接收端:数据的接收方进程,称为接收端,此主机可以称为目的主机
收发端:发送端和接收端两端
在这里插入图片描述

  1. 请求和响应

当进程A可以请求数据发送,进程B就响应数据的发送

3.客户端和服务器

客户端:获取服务一方的进程
服务器:提供服务一方的进程,可以对外提供服务

在这里插入图片描述

Socket套接字

概念

Socket套接字,是系统提供用于网络通信的技术,基于这个Socket编程网络程序开放就是网络编程

分类

1.流套接字:TCP协议,即Transmission Control Protocol(传输控制协议)
特点:有连接、可靠传输、面向字节流、全双工、有接收和发送缓冲区、大小无限
2.数据报套接字:UDP协议,User Datagram Protocol(用户数据报协议)
特点:无连接、不可靠传输、面向数据报、全双工、有接收缓冲区无发送缓冲区、大小受限一次最多传输64KB
3.原始套接字:自定义传输层协议,用于读写内核没有处理的IP协议数据

1.有连接/无连接:通信双方是否互相保存对方的核心信息(IP和端口号)
TCP就保存了,在结束的时候就释放了,UDP没有保存,但是传输信息中也是有相关信息的
2.可靠传输/不可以靠传输可靠传输就是丢包的概率,像TCP可靠传输,其丢包的概率比较低,UDP不可靠传输,不关心丢不丢包
3.面向字节流和数据报:面向字节流,读写文件比较方便可以使用InputStream和OutputStream ;面向数据报:每次读取只可以以一个UDP为单位进行
4.全双工和半双工:全双工:可以双向传输,半双工:只可以单向传输

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

UDP数据报套接字编程

Java提供的一些API介绍
DatagramSocket

方法说明
DatagramSocket()创建一个UDP数据报套接字Socket,绑定本机任意一个端口号
DatagramSocket(int port)创建一个UDP数据报套接字Socket,绑定到指定port端口号

端口号0~65535,但是 0 - 1023其有特殊含义不可以选择
这里一般情况下,不带参数的都用于客户端,一个服务器可能对应多个客户端,但是这些端口号不可以相同,如果我们自己手动创建可能会出现相同,因此让操作系统任意分配,这样可以有效防止端口号相同从而导致的冲突,手动指定端口号,带参数的构造方法一般由于服务器

方法说明
void reveive(DatagramPacket p)从p套接字接收数据报
void send(DatagramPacket p)从套接字发送数据报
void close()关闭数据报套接字

这里的receive方法,如果没有接收到数据报,其就会进行阻塞等待
而send不会阻塞等待,直接发送
DatagramPacket

方法说明
DatagramPacket(byte buf[], int offset, int length)构造一个DatagramPacket对象来接收数据报,保存在buf数组中,接收长度为length
DatagramPacket(byte buf[], int offset, int length, SocketAddress address)同理,address是目的主机的IP和端口号
方法说明
InetAddress getAddress()从接收数据报中,获取发送端的IP地址,从发送数据报中,获取接收端IP地址
int getPort()同理,用于获取端口号
byte[] getData()获取数据报中的数据

InetSocketAddress
因为上面的DatagramPacket有一个参数是SocketAddress,这里的这个是其子类

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

回显实例

服务器

1.此时的构造函数,根据自己提供的端口号进行实例化DatagramSocket 对象

在这里插入图片描述

2.启动过程
获取客户端请求

在这里插入图片描述

先创建一个DatagramPacket用于存放请求,并且要先申请好空间
并且使用receive接收请求,此方法中的参数是输出型参数,请求的内容会放入到requestPacket对象上

计算响应
此处是回显服务器,不做任何处理,将请求方发送信息发回去即可

将请求的结果返回给客户端
此处要将计算响应返回的字符串,转换成DatagramPacket返回给服务器

在这里插入图片描述
并且这里不仅要将数据放入进去,也要将发送请求的客户端对应的IP和端口号放入进去,这样才可以找到客户端

//udp回显服务器
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("server start");
while (true){
//1.先获取请求,此时requestPacket是一个输出型参数
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
//将二进制转换成字符串
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());
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s : %d],request:%s,response : %s",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
System.out.println();
}
//此处的计算响应直接返回即可
public  String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
//这里的端口号不可以重复
UdpEchoServer server = new UdpEchoServer(9900);
server.start();
}
}

客户端

构造方法,这里客户端并不需要指定端口号,让系统自动分配即可,因为这里客户端比较多,出现重复概率较高,让系统自动分配恰好解决了这一痛点
虽然我们不需要指定端口号,但是这个我们要确定客户端的IP和端口号,因为这样服务器进行接收数据时候才可以通过客户端的IP和端口号,将数据返回给客户端

在这里插入图片描述

启动客户端

1.先进行输入

在这里插入图片描述
2.将输入字符串变成一个DatagramPacket对象传给服务器,虽然UDP并不需要保存对方的核心信息(IP和端口号),但是也要知道这些信息才可以找到对应的IP和端口号
在这里插入图片描述
在这里插入图片描述
3.接收服务器返回的数据
依旧使用一个DatagramPacket对象进行接收,并且这里receive参数是输出型参数
在这里插入图片描述

//客户端
public class UdpEchoClient {
//创建socket对象
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
private UdpEchoClient(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();//客户端不需要指定端口号,系统自动分配
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("client start");
Scanner scanner = new Scanner(System.in);
//通过客户端将这个发送给服务器
while (true){
//1.用户进行输入
System.out.print("->");
String request = scanner.next();
if(request.equals("exit")){
break;
}
//2.将用户写入字符串构造成UDP数据报,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length,
InetAddress.getByName(this.serverIp),//将这个Ip转换成InetAddress
this.serverPort);
socket.send(requestPacket);
//3.从服务器中读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);//输出型参数
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",9900);
client.start();
}
}

客户端和服务器结合使用
在这里插入图片描述
exit结束客户端
在这里插入图片描述
这里可以一个服务器对应多个客户端
在这里插入图片描述
服务器
在这里插入图片描述
三个不同的客户端
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里可以将这个服务器修改一下,让其变成一个查询字典的功能
此时只需要一个新的类来继承上面的UdpEchoServer这个服务器即可,我们这里只有处理过程变了而已,所以继承重写一下process方法即可

public class UdpDictServer extends UdpEchoServer{
private Map<String ,String> map = new HashMap<>();public UdpDictServer(int port) throws SocketException {super(port);//这里直接将对应关系建立起来即可map.put("hello","你好");map.put("world", "世界");map.put("cat", "小猫");map.put("dog", "小狗");map.put("pig", "小猪");map.put("like","喜欢");}@Overridepublic String process(String request) {return map.getOrDefault(request,"没有找到这个单词");}public static void main(String[] args) throws IOException {UdpDictServer server = new UdpDictServer(9900);server.start();}}

直接使用一个哈希表,将其对应关系单词和中文对应关系即可
在这里插入图片描述
在这里插入图片描述

TCP流套接字编程

ServerSocket
这个是创建TCP服务器端Socket的API
构造方法

方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,绑定port端口号
方法说明
Socket accept()接收客户端连接,有客户端连接就,返回一个Socket对象,基于这个与客户端进行连接,否则阻塞等待
void close()关闭此套接字

因为其创建对象也会占用空间,因此结束所有操作,要将这个数据给释放,否则会导致内存泄漏

Socket
Socket是客户端Socket,其是在双方进行联系的时候,保存对端的信息及其接受发数据

方法说明
Socket(String host,int port)创建一个客户端流套接字,与对应IP和端口号进建立连接

方法

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

回显实例

TCP服务器
1.构造函数同理使用,需要自己手动指定端口号
在这里插入图片描述
启动服务器
1.这里需要使用Socket流套接字对象来接收其请求
在这里插入图片描述
2.计算响应(这里是回显服务器,所有直接将其接收的数据返回即可)
3.返回
在这里插入图片描述
这里的ServerSocket负责接收数据,但是处理数据是通过Socket
这里ServerSocket就像酒店前台,在前台开了房间,而Socket就像酒店服务员将你带到你所在的房间

这里Socket对象是一个文件,因此可以使用这些关于文件的操作,进行读取和写入

public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//如果线程较多的话,其频繁的创建和销毁也是比较浪费时间的,因此可以使用线程池
ExecutorService executorService = Executors.newCachedThreadPool();
public void start() throws IOException {
System.out.println("server start");
while (true){
//使用accept进行连接,并且会返回Socket对象
Socket socket = serverSocket.accept();
//通过这个方法进入处理连接过程,并且一旦调用就不会调用accept
processConnection(socket);
//因为这里会进行等待,导致这里只可以有一个客户端进行操作,因此这里可以使用多线程
//但是使用多线程多了,其销毁和创建也需要时间
//            Thread thread = new Thread(() ->{
//               processConnection(socket);
//            });
//            thread.start();
executorService.submit(() ->{
processConnection(socket);
});
}
}
public void processConnection(Socket socket){
System.out.printf("[%s:%d服务器上线",socket.getInetAddress(),socket.getPort());
System.out.println();
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
//1.读取并进行解析
if(!scanner.hasNext()){
System.out.printf("[%s:%d服务器下线",socket.getInetAddress(),socket.getPort());
break;
}
//2.根据请求计算响应
String request = scanner.next();
String response = process(request);
//3.返回
writer.println(response);
writer.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
request, response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
//这里完成以后要进行释放内存空间
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
//直接返回即可
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9900);
tcpEchoServer.start();
}
}

客户端
构造方法,此时这里需要指定IP和端口号
在这里插入图片描述
启动客户端
这里依旧使用Scanner和writer分别进行发送和读取数据

在这里插入图片描述

public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//因为其是连接的,所以需要服务器Ip 和 端口
socket = new Socket(serverIp, serverPort);
//socket.connect(new InetSocketAddress((InetAddress.getByName(serverIp)),serverPort));
}
public void start(){
System.out.println("client start");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
//1.从控制台输入
System.out.print("->");
String request = scanner.next();
//2.将请求发送给服务器
writer.println(request);
writer.flush();//去除缓冲区
//3.接收
String response = scannerNetwork.next();
System.out.println(response);
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9900);
tcpEchoClient.start();
}
}

在这里插入图片描述
在这里插入图片描述
虽然这样可以正常使用这个,但是如果换成多个客户端,这里会出现问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里就会出现这样的问题,只有一个服务器在操作,其他服务器都没有正常工作
在这里插入图片描述

引入多线程

因为上面无法连接多个客户端,因此这里我们可以将其设置成多多线程

public void start() throws IOException {
System.out.println("server start");
while (true){
//使用accept进行连接,并且会返回Socket对象
Socket socket = serverSocket.accept();
//通过这个方法进入处理连接过程,并且一旦调用就不会调用accept
// processConnection(socket);
//因为这里会进行等待,导致这里只可以有一个客户端进行操作,因此这里可以使用多线程
//但是使用多线程多了,其销毁和创建也需要时间
Thread thread = new Thread(() ->{
processConnection(socket);
});
thread.start();
}
}

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

引入线程池

但是当线程变多了,其创建和销毁也会比较浪费时间,因此这里就可以使用线程池

ExecutorService executorService = Executors.newCachedThreadPool();
public void start() throws IOException {
System.out.println("server start");
while (true){
//使用accept进行连接,并且会返回Socket对象
Socket socket = serverSocket.accept();
executorService.submit(() ->{
processConnection(socket);
});

其实可以进一步优化为IO多路复用,就是一个线程对应多个客户端,当某一个客户端需要处理时候,这个客户端对应的socket才会过来处理

细节处理

内存泄漏问题

在这里插入图片描述
在其服务器中会不断的创建socket,因此当这个对象不实用的时候,需要进行释放资源

缓冲区问题

在这里插入图片描述
这里的writer里面内置了缓冲区,为了避免重复的写入数据,其达到一定程度,当其缓冲区满了,其才是真正的写入,因此这里使用flush进行冲刷缓冲区,这样就不满了才是真正的写入

隐藏条件
TCP中,其请求和响应都是以\n进行结尾

在这里插入图片描述
当然这个也可以像UDP那样写一个查字典功能

public class TcpDIctServer extends TcpEchoServer{
private Map<String,String > map = new HashMap<>();public TcpDIctServer(int port) throws IOException {super(port);//这里直接将对应关系建立起来即可map.put("hello","你好");map.put("world", "世界");map.put("cat", "小猫");map.put("dog", "小狗");map.put("pig", "小猪");}@Overridepublic String process(String request) {return map.getOrDefault(request,"没找到这个单词");}public static void main(String[] args) throws IOException {TcpDIctServer tcpDIctServer = new TcpDIctServer(9900);tcpDIctServer.start();}}

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询