看到大佬的帖子,Standford CS144 (24 Winter) Computer Network - 可能是年轻人最好的现代 C++ 入门课https://zhuanlan.zhihu.com/p/20551290958,很想恶补一下自己渣渣的C++水平,同时也希望给自己加点项目实践的经历
文章目录
环境配置
在我的ubuntu中安装下面的包
sudo apt update && sudo apt install git cmake gdb build-essential clang \ clang-tidy clang-format gcc-doc pkg-config glibc-doc tcpdump tshark
1 Networking by hand
手动使用网络
Telnet的使用
在日常的工作和网络调试中,经常需要探测当前主机是否能够连接另外一台主机的相关端口,这时候我们就可以使用telnet命令来进行测试连接是否畅通
1.1 Fetch a Web page
Example:抓取网页,以http://cs144.keithw.org/hello为例
告诉 telnet
程序打开一个到 cs144.keithw.org
服务器的可靠字节流连接,并指定服务为 http
(超文本传输协议)。
telnet cs144.keithw.org http
#回车
#GET表示向服务器发起请求,/hello表示网址第三个斜杠及之后的内容,HTTP/1.1表示采用1.1版本的http协议
GET /hello HTTP/1.1
#Host 是一个请求头字段,用于指定请求的服务器的域名(和可选的端口号)
Host: cs144.keithw.org
#两下回车 开始连接
输出:
Hello,CS144!
#连接成功
Assignment:抓取这个URLhttp://cs144.keithw.org/lab0/sunetid
telnet cs144.keithw.org http
#回车
#GET表示向服务器发起请求,/hello表示网址第三个斜杠及之后的内容,HTTP/1.1表示采用1.1版本的http协议
#Host 是一个请求头字段,用于指定请求的服务器的域名(和可选的端口号)
GET /lab0/sunetid HTTP/1.1
Host: cs144.keithw.org
#两下回车 开始连接
输出:
Hello,CS144!
#连接成功
1.2 Send yourself mail
telnet smtp.qq.com 587
HELO qq.com
AUTH LOGIN
MjU1NjA2Njg4NQ==
b2p5cXJncHB1aWJnZWNnZQ0K
唉,输入授权码之后一直报错,不知道怎么办了(摊手手)
1.3 Listening and connecting
首先安装netcat,https://blog.csdn.net/qq_30653631/article/details/93749505
netcat(通常缩写为nc)是一个功能强大的网络工具,它不仅可以用于网络调试和分析,还可以用于创建简单的服务器和客户端。netcat能够监听网络端口,接收进入的连接,并转发数据到标准输出或从标准输入转发数据到网络。
#-v:详细模式;-l:进入监听模式,等待客户端的连接;-p:监听的端口号
netcat -v -l -p 9090
新建一个终端窗口
telnet 客户端可以连接到支持 telnet 协议的服务器上,执行远程命令或者与服务器进行交互。
telnet localhost 9090
然后,这两个窗口就可以互相发消息了
2 Writing a network program using an OS stream socket
使用OS套接字流编写网络程序。
利用Linux内核的特性——socket stream(在两个程序之间创建可靠的双向字节流),一端是我电脑上的程序,一点是web server,套接字就像一个文件描述符(就像标准输入/输出流一样),当二者连接时,写入一个套接字的任何字节将会以相同的顺序从另一台计算机上的套接字出来。
编写“webget”程序,创建TCP流套接字,以连接到服务器并获取页面。
实现webget.cc的get_URL()
这个函数要完成的功能:
建立TCP连接
向目标端口发起http请求
- 需要创建套接字
先来看看TcpSocket的定义吧
//! A wrapper around [TCP sockets](\ref man7::tcp)
class TCPSocket : public Socket
{
private:
//! \brief Construct from FileDescriptor (used by accept())
//! \param[in] fd is the FileDescriptor from which to construct
explicit TCPSocket( FileDescriptor&& fd ) : Socket( std::move( fd ), AF_INET, SOCK_STREAM, IPPROTO_TCP ) {}
public:
//! Default: construct an unbound, unconnected TCP socket
TCPSocket() : Socket( AF_INET, SOCK_STREAM ) {}
//! Mark a socket as listening for incoming connections
void listen( int backlog = 16 );
//! Accept a new incoming connection
TCPSocket accept();
};
TCPSocket的父类是Socket,它有两种构造方式
前面是接受一个FileDescriptor类型的参数,用于从已存在的文件描述符创建TCP套接字。
默认构造函数,不需要任何参数,直接创建一个基础的TCP套接字。
//! Construct from a file descriptor.
Socket( FileDescriptor&& fd, int domain, int type, int protocol = 0 );
//! Construct via [socket(2)](\ref man2::socket)
Socket( int domain, int type, int protocol = 0 );
调用webget进行测试
在
\build
路径下运行make
命令来编译程序运行
./apps/webget cs144.keithw.org /hello
来测试你的程序。这个命令会向你的程序发送一个请求,类似于在网络浏览器中访问http://cs144.keithw.org/hello
。
Ok,发现和前面Fetch a Web page的输出是一样的
webget主函数发生了什么
(本菜鸟的学习时间)
int main( int argc, char* argv[] )
{
try {
if ( argc <= 0 ) {
abort(); // For sticklers: don't try to access argv[0] if argc <= 0.
//abort:标准库函数,用于立即终止程序执行
}
//args用于接收参数
auto args = span( argv, argc );
// The program takes two command-line arguments: the hostname and "path" part of the URL.
// Print the usage message unless there are these two arguments (plus the program name
// itself, so arg count = 3 in total).
if ( argc != 3 ) {
cerr << "Usage: " << args.front() << " HOST PATH\n";
cerr << "\tExample: " << args.front() << " stanford.edu /class/cs144\n";
//标准错误输出流,用于输出错误信息和使用说明
return EXIT_FAILURE;
}
// Get the command-line arguments.
const string host { args[1] };
const string path { args[2] };
// Call the student-written function.
get_URL( host, path );
} catch ( const exception& e ) {
cerr << e.what() << "\n";
//what:标准异常类的成员函数,返回一个描述异常的字符串
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
//EXIT_SUCCESS(值为0)表示程序正常结束
//EXIT_FAILURE(值为非0)表示程序异常结束
}
1. C++的主函数也是可以传参的!
C++的主函数可以接收命令行参数,这是C++程序的标准特性。主函数有两种标准形式:
int main() // 不接收参数的形式
int main(int argc, char* argv[]) // 接收命令行参数的形式
比如,当你执行程序时(如./apps/webget cs144.keithw.org /hello
),操作系统会:
计算参数个数(包括程序名)自动将这个数量赋值给argc
将所有参数组织成字符串数组传给argv
argv[0] 是程序名
argv[1] 开始是实际的命令行参数
2. 使用try-catch块包裹所有的代码
try-catch
语句用于异常处理
try {
// 可能抛出异常的代码块
}
catch (异常类型1 异常对象名1) {
// 处理异常类型1的代码逻辑
}
catch (异常类型2 异常对象名2) {
// 处理异常类型2的代码逻辑
}
// 可以根据需要添加多个catch块,分别处理不同类型的异常
3. 其他
span
span:是 C++20 引入的一个类模板,定义在 <span> 头文件中,它提供了对连续内存区域(比如数组、std::vector 等容器中存储元素的内存区域)的一种非拥有性视图。
简单来说,span 可以让你像操作数组一样去操作一段连续的内存,不管这段内存实际是来源于数组、容器,还是其他连续内存分配的情况,并且同样不进行数据的拷贝,只是描述了内存区域的范围(起始地址和长度)。
在C++中,视图(view)是一种轻量级的抽象,它提供了访问底层数据的接口,但不拥有数据的所有权。
遇到问题:
lily@lily-KLVL-WXX9:~/minnow/build$ cmake --build . --target check webget
make: *** 没有规则可制作目标“check”。 停止。
先不管了,往下看吧
3 An in-memory reliable byte stream
要求我们实现一个内存中可靠的双向字节流
(没怎么看懂,仿佛说了又没说,让我们直接切到minnow/src/byte_stream.hh
和minnow/src/byte_stream.hh
看看代码框架吧!
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
class Reader;
class Writer;
class ByteStream
{
public:
explicit ByteStream( uint64_t capacity );
// Helper functions (provided) to access the ByteStream's Reader and Writer interfaces
Reader& reader();
const Reader& reader() const;
Writer& writer();
const Writer& writer() const;
void set_error() { error_ = true; }; // Signal that the stream suffered an error.
bool has_error() const { return error_; }; // Has the stream had an error?
protected:
// Please add any additional state to the ByteStream here, and not to the Writer and Reader interfaces.
uint64_t capacity_;
bool error_ {};
};
class Writer : public ByteStream
{
public:
void push( std::string data ); // Push data to stream, but only as much as available capacity allows.
void close(); // Signal that the stream has reached its ending. Nothing more will be written.
bool is_closed() const; // Has the stream been closed?
uint64_t available_capacity() const; // How many bytes can be pushed to the stream right now?
uint64_t bytes_pushed() const; // Total number of bytes cumulatively pushed to the stream
};
class Reader : public ByteStream
{
public:
std::string_view peek() const; // Peek at the next bytes in the buffer
void pop( uint64_t len ); // Remove `len` bytes from the buffer
bool is_finished() const; // Is the stream finished (closed and fully popped)?
uint64_t bytes_buffered() const; // Number of bytes currently buffered (pushed and not popped)
uint64_t bytes_popped() const; // Total number of bytes cumulatively popped from stream
};
/*
* read: A (provided) helper function thats peeks and pops up to `len` bytes
* from a ByteStream Reader into a string;
*/
void read( Reader& reader, uint64_t len, std::string& out );
不慌,先问一下ai
ByteStream 是一个实现了流式数据处理的类设计,主要包含三个核心组件:
- ByteStream 基类:
管理基础的流容量(capacity_)和错误状态(error_)
提供 Reader 和 Writer 接口访问方法
- Writer 类(写入端):
push(): 写入string数据到流中,但要考虑可用容量限制
close(): 关闭流,表示不再写入
提供状态查询方法:
is_closed(): 是否已关闭
available_capacity(): 当前可用容量
bytes_pushed(): 已写入的总字节数
- Reader 类(读取端):
peek(): 预览缓冲区中的下一块数据
pop(): 从缓冲区移除指定长度的数据
提供状态查询方法:
is_finished(): 是否完成(关闭且全部读取)
bytes_buffered(): 当前缓冲区字节数
bytes_popped(): 已读取的总字节数
好的现在任务已经明确了,就是把上面的函数都实现,好多函数啊,先从哪儿入手呢
先从这种只需要个返回值的只读函数开始吧
由于这种需要返回值的函数
需要一个返回值,因此要在ByteStream中添加一些关键的成员变量来跟踪状态:
【string 前记得加std::,前面没有用命名空间,不加的话会导致变量定义失败】
下面就是慢慢实现声明的各个函数
#include "byte_stream.hh"
using namespace std;
ByteStream::ByteStream(uint64_t capacity) : capacity_(capacity), bytes_pushed_(0), bytes_popped_(0), is_closed_(false) {}
//往缓存区写入数据
void Writer::push( string data )
{
//只需考虑缓存区容量,缓存区容量大于data的长度,直接写入即可
// if (buffer_.size() + data.size() <= available_capacity()) {
// buffer_ += data;
// bytes_pushed_ += data.size();
// }
uint64_t available = available_capacity();
uint64_t to_push = std::min(available, static_cast<uint64_t>(data.size()));
if (to_push > 0) {
buffer_.append(data.substr(0, to_push));
bytes_pushed_ += to_push;
}
}
//关闭写入流
void Writer::close()
{
is_closed_ = true;
}
//判断写入流是否关闭
bool Writer::is_closed() const
{
return is_closed_;
}
//返回缓存区剩余容量
uint64_t Writer::available_capacity() const
{
return capacity_ - buffer_.size();
}
//返回写入流写入的字节数
uint64_t Writer::bytes_pushed() const
{
return bytes_pushed_;
}
//查看缓冲区接下来的字节数据
string_view Reader::peek() const
{
//bytes_buffered():缓冲区已缓冲的字节数
return string_view(buffer_).substr(0, bytes_buffered());
}
//从缓冲区弹出len个字节
void Reader::pop( uint64_t len )
{
//如果len大于缓冲区已缓冲的字节数,直接弹出缓冲区所有字节
if (len >= bytes_buffered()) {
buffer_.clear();
bytes_popped_ += bytes_buffered();
}
//否则,弹出len个字节
else {
buffer_.erase(0, len);
bytes_popped_ += len;
}
}
//检查读取流是否已完成
bool Reader::is_finished() const
{
//是否所有数据都已读取且写入端已关闭
return bytes_buffered() == 0 && is_closed_;
}
//返回缓冲区已缓冲的字节数
uint64_t Reader::bytes_buffered() const
{
return bytes_pushed_ - bytes_popped_;
}
//返回读取流读取的字节数
uint64_t Reader::bytes_popped() const
{
return bytes_popped_;
}
另:关于push直接写入or支持部分写入
若使用buffer_ += data,即只有当data的大小小于可写容量的时候才会写入,而第二种方法支持部分写入
//往缓存区写入数据
void Writer::push( string data )
{
//只需考虑缓存区容量,缓存区容量大于data的长度,直接写入即可
// if (buffer_.size() + data.size() <= available_capacity()) {
// buffer_ += data;
// bytes_pushed_ += data.size();
// }
uint64_t available = available_capacity();
uint64_t to_push = std::min(available, static_cast<uint64_t>(data.size()));
if (to_push > 0) {
buffer_.append(data.substr(0, to_push));
bytes_pushed_ += to_push;
}
}
补一些基础中的基础
uint64_t
无符号的64位整数类型,它在 C 和 C++ 编程语言中定义于 <cstdint>
或 <stdint.h>
头文件中。
应用:
大数处理
系统编程:表示内存地址、文件大小、时间戳
网络编程:存储网络统计数据,如数据包的容量或字节数
const声明函数
uint64_t Writer::available_capacity() const
{
return capacity_ - num_bytes_buffered_;
}
作用:
- 保证不修改成员变量:只读取数据,不会改变对象的状态