
你可曾有过这般困惑,每日所调用的接口,所访问的网站,其背后究竟凭借什么达成通信呢?好多人运用了数年框架,竞就连最为基础的socket编程都一毫不知,碰到怪异的bug之时,只能四处去查找资料。今日,我们借助Rust标准库,不去依赖任何框架,从无到有搭建起一个能够处理多客户端连接的TCP服务器,从而能够让你亲自去揭开网络通信那神秘的面纱。
Rust这门语言颇不寻常,其标准库自身携有完备的网络功能模块,并非如别的语言那般需额外去安装第三方包。仅需于代码起始处写上“use std::net::TcpListener”和“Use std::io::prelude::*”,便能够径直调用与网络相关的工具。
如此这般去做所具备的好处是十分显著的,代码呈现出干净的状态,不存在繁杂的依赖关系,哪怕是新手也能够一眼就明白每一行代码究竟在进行怎样的操作。你能够将注意力全然聚焦于 TCP 通信的核心流程之上,而并非被框架的各类配置弄得头昏脑涨。对于那些想要弄清楚底层原理的人而言,这是最为直接的途径。
服务器若要被客户端寻得,首先需于某个端口处施行监听,恰似你若要开设一家店铺,得先具备一个门牌号,我们挑选8080端口,缘由在于此端口于开发环境里鲜少被占用,并且诸多教程皆采用它,便于你开展测试,代码仅有一行:“let listener = TcpListener::bind(127.0.0.1:8080).unwrap();”。
use std::net::TcpListener;
fn main() {
// 绑定本地8080端口,作为服务器的监听端口
let listener = TcpListener::bind("127.0.0.1:8080")
.expect("Failed to bind address"); // 绑定失败时给出提示
println!("Server running on port 8080"); // 启动成功提示
}

这句代码做了三件事情,其一向操作系统申请使用8080端口,其二告诉操作系统“这个端口归我了”,其三而后启动监听模式。要是端口被其他程序占用,bind函数将会返回错误,在这个时候unwrap就会致使程序崩溃并且打印错误信息。在实际代码当中,你能够用match来处理这种异常,不过教学示例里unwrap足够清晰。
// 承接第一步的代码,在main函数中添加循环,监听客户端连接
for stream in listener.incoming() {
match stream {
Ok(_) => println!("Client connected"), // 客户端连接成功
Err(e) => println!("Connection failed: {}", e), // 连接失败提示错误
}
}
当服务器开启监听之后,便需要一个循环用以持续接收客户端的连接请求,此循环会持续运行,直至你手动终止程序,每当有客户端尝试连接之际,TcpListener的accept方法就会返回一个全新的TcpStream对象,该对象表征客户端与服务器之间的连接通道。
你能够将TcpStream视作一根管道,数据借助这根管道朝着两个方向流动。要是连接出现失败状况,像是客户端突然断掉网络或者服务器负载过度高,accept就会返回错误。在这个时候,我们把错误信息打印出来,之后持续等待下一个连接,而非致使整个服务器崩溃。这属于生产环境代码的基本素养。
use std::io::Read;
use std::net::TcpStream;
// 定义处理客户端请求的函数,接收TcpStream(通信管道)作为参数
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 512]; // 定义一个512字节的缓冲区,用于存储客户端消息
stream.read(&mut buffer).unwrap(); // 读取客户端消息,存入缓冲区
// 将缓冲区的内容转换成字符串,打印客户端消息
println!("Message: {}", String::from_utf8_lossy(&buffer));
}

连接建立之后,服务器得能够读取客户端发送过来的消息。我们得去创建一个缓冲区呀,就像“let mut buffer = [0; 1024]”这样的,用来暂且存放读取得到的数据。随后调用stream.read(&mut buffer)方法,此方法会使当前线程阻塞,直至读到数据或者连接断掉。
use std::io::Write; // 引入写入模块
// 在handle_client函数中添加发送响应的代码
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
println!("Message: {}", String::from_utf8_lossy(&buffer));
// 向客户端发送响应消息
stream
.write(b"Hello from Rust TCP Server\n") // 响应内容
.unwrap(); // 写入失败时直接终止程序(新手可暂时这样处理)
}
你所读取到的数据为字节数组,倘若你发送的是文本,那么能够借助“String::from_utf8_lossy”将其转化成字符串。在进行测试之时,开启终端并输入“telnet localhost 8080”,接着随意输入几个字,服务器便会把这些内容予以打印出来。需要注意的是,telnet属于Mac以及Linux系统自身所带的工具,Windows用户要先于控制面板之中开启Telnet客户端功能。
具备仅能接收消息功能那样的服务器是不足够,于客户端发送请求之后,通常会期望服务器返回某些内容。我们要引入Write trait,接着调用stream.write_all方法。此方法会将数据写入至连接通道里,客户端那边便能够收到。
use std::thread; // 引入线程模块
// 修改main函数中的循环,为每个连接创建线程
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080")
.expect("Failed to bind address");
println!("Server running on port 8080");
for stream in listener.incoming() {
let stream = stream.unwrap(); // 处理连接失败的情况
// 为每个客户端连接创建一个新线程,执行handle_client函数
thread::spawn(|| {
handle_client(stream);
});
}
}
当把消息读取完毕后,添加上一行“stream.write_all(bHello from Rust TCP Server\n).unwrap();”,不管客户端发送什么样的消息,都能够收到这串回复。你能够尝试在telnet当中发送“hello”,瞧瞧会不会收到服务端给出的回应。到了这个时候,一个具备双向通信功能的TCP服务器就构建完成了。

在前面的代码当中,存在着一个规模较大的问题,那就是:它在同一时间之内,仅仅能够对一个客户端进行处理。要是出现了第二个客户端连接进入的情况,那么就必然需要等待第一个客户端断开连接之后,才可以获得服务。这明显是不符合实际情况的。而解决这个问题的方法是比较简单易懂的:每当接收到一个全新的连接时,就应当创建出一个独立的线程,以此来对其进行处理。
有观点讲当下全都属于框架开发,去学习那些底层方面的事物能有啥用处呢?然而你仔细思索一番,当碰到接口延迟、连接超时以及内存泄漏这类疑难杂症之际,仅仅懂得框架的人员只能四处去搜寻解决办法,可是懂得底层的人员却能够直接确定问题所在。TCP握手、缓冲区管理、阻塞与非阻塞IO,这些知识在任何一种语言以及框架当中都是通用的。
虽这个由手动搭建而成的服务器挺简易,可其涵盖着生产环境的基础构架,即监听,接收,读写,并发。等你往后再度用任何Web框架之际,你心里明晰框架于背后为你做了啥,出了问题亦晓得从哪一方向去排查。这正是从“会运用工具”迈向“理解工具”的关键一步。
尾端之际问你一项问题,于你日常开展开发期间,究竟是会凭借自身主动耗费时间去研习诸如socket、TCP这类底层的知识内容,还是仅仅安于能够施展框架去调用接口呢,欢迎处在评论区域之中分享自身的经历情况,点赞以使更多之人目睹这篇具备干货性质的内容。