创建于 2020-07-31,最终修订于 2020-08-06
在之前的介绍课程(零)中我们说了要用六节课来实现一个HTTP,
今天的任务是 “[易]简单的Cmake的教程,选用一个Socket库并实现一个echo”。
所有的代码都在 https://github.com/dashjay/http_demo/tree/master/1-cmake-socket-echo 中
Let’s do it
0x1 编写一个HelloWorld并尝试使用Cmake编译它
1
2
3
4
5
|
#include<iostream>
int main(){
std::cout << "Hello World!" << '\n';
}
|
我们本可以使用如下命令进行编译并且运行
1
2
3
|
g++ -o main main.cpp
./main
Hello World!
|
但是我们要尝试使用一次Cmake[1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 指定cmake的版本,随意了,我们不会用太高级的功能
cmake_minimum_required(VERSION 3.16)
# set可以设定一个变量
set(project_name http_demo)
# 项目名称 ${xxxx} 可以导入之前的变量
project(${project_name})
set(CMAKE_CXX_COMPILER "c++") # 编译器
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_CXX_STANDARD 17) # cpp指定标准
# 设定编译的FLAGS
set(CMAKE_CXX_FLAGS -g -W)
string(REPLACE ";" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
message(STATUS "CMAKE_CXX_FLAGS: " "${CMAKE_CXX_FLAGS}")
# 添加一个可执行文件
add_executable(main main.cpp)
|
通常情况下我们不会再项目目录里执行 cmake .
,这会导致很多混乱的问题,我们通常创建一个build文件夹做这个操作
1
2
3
4
|
mkdir build; cd build
cmake .. && make
./main
Hello World!
|
build目录下会产生很多文件,通常我们不用理会他们,只需要管我们的输出文件即可。
为什么弄这么麻烦的东西
make新手教程
当项目复杂到一定程度时,有大量自动化的操作可以节省我们的时间,例如我们可以针对Hello World输出生成一个测试,每次编写完程序,我们只需要make一下(如果cmake文件没有变化的话),然后运行make test来进行自动化测试(测试样例需要我们自己编写)。当然这个Cmake在本次项目中的作用也不是很能体现出优势,但是笔者认为仍然有必要学习这种自动化工具。
0x2 选用一个cpp的socket库
经过一些挑选,选中了一个叫Sockpp的库。这个库用法很“现代”CPP,十分舒适,有很多上古socket库,用法或者代码比较古老了,我就只推荐这个了。
安装也很简单
1
2
3
|
git clone https://github.com/fpagliughi/sockpp
cd sockpp; mkdir build; cd build
cmake .. && make && sudo make install
|
这样,sockpp就安装到你系统中了,确切的说是分别安装了头文件和依赖库到你系统的 include 和 lib 中。
1. 了解sockpp的使用
我们在本节课中暂时只简单的使用 tcp 接收一些数据,因此我们在本节课程里只引入 sockpp 的一个头部 #include "sockpp/tcp_acceptor.h"
这样几行代码,就可以指定端口绑定,监听,等待连接的到来。
1
2
3
4
5
|
sockpp::tcp_acceptor acc(port);
if (!acc){
std::cerr << acc.last_error_str() << '\n';
exit(1)
}
|
使用accept函数就可以和到来的连接创建一个socket链接,
1
|
sockpp::tcp_socket sock = acc.accept();
|
2. 写一个Echo
不多废话,直接看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
#include "sockpp/tcp_acceptor.h"
#include <iostream>
void print_without_crlf(char *ptr);
int main(){
// 指定监听端口
int16_t port = 8080;
// 使用端口启动监听
sockpp::tcp_acceptor acc(port);
if (!acc){
std::cerr << acc.last_error_str() << '\n';
exit(1);
}
std::cout << "try accept a conn at port " << port << '\n'
// 接收连接请求
sockpp::tcp_socket sock = acc.accept();
// 创建缓存
char buf[1024];
// 循环接收打印并且echo
for(;;){
if(auto siz = sock.read(buf, 1024); siz > 0){
print_without_crlf(buf);
sock.write(buf, siz);
}else{
// siz < 0(断开连接)会退出
break;
}
}
std::cout << "closed" << '\n';
}
// 这个函数为什么这么写稍后解释
void print_without_crlf(char *ptr){
std::cout << "recv: ";
for(;;){
if(*ptr == '\r' || *ptr == '\n'){
break;
}
std::cout << *ptr;
ptr+=1;
}
std::cout << '\n';
}
|
3. Echo代码编译运行
我们首先尝试直接使用 g++ 编译这串代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
g++ -o server server.cpp
# 以下是输出
~/CLionProjects/http_demo » g++ -o server server.cpp
In file included from server.cpp:1:
In file included from /usr/local/include/sockpp/tcp_acceptor.h:48:
In file included from /usr/local/include/sockpp/acceptor.h:48:
In file included from /usr/local/include/sockpp/inet_address.h:50:
/usr/local/include/sockpp/sock_address.h:94:3: warning: 'auto'
type specifier is a C++11 extension [-Wc++11-extensions]
auto p = sockaddr_ptr();
^
/usr/local/include/sockpp/sock_address.h:116:9: error: unknown
type name 'constexpr'
static constexpr size_t MAX_SZ = sizeof(sockaddr_storage);
|
为什么直接编译会这样报错
我不知道 g++(clang++) 默认使用什么版本,也许是 c++11 或者 c++98 但是因为 sockpp 是一个比较新的库,我们这里推荐指定 c++11 以上 c++17 当然更好了,在 C++17 中还可以使用 if(STATEMENT;CONDITIONS){}
这种用法,美滋滋。
上方的有关 cpp 版本的描述不是十分专业,请自行查询更详细的资料
1
2
3
4
5
6
|
c++03 c++2a gnu++03 gnu++2a iso9899:1990
c++11 c++98 gnu++11 gnu++98 iso9899:199409
c++14 c11 gnu++14 gnu11 iso9899:1999
c++17 c89 gnu++17 gnu89 iso9899:2011
c++1y c90 gnu++1y gnu90 iso9899:2017
c++1z c99 gnu++1z gnu99
|
1
2
3
4
5
6
7
|
g++ -std=c++17 -o server server.cpp
# 以下是输出
Undefined symbols for architecture x86_64:
"sockpp::inet_address::create(unsigned int, unsigned short)", referenced from:
sockpp::inet_address::inet_address(unsigned short) in server-40d3ec.o
"sockpp::stream_socket::read_timeout(std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000000l> > const&)", referenced from:
....
|
在指定了版本之后,为什么还是会报错,因为sockpp不仅仅是一个头部申明,他复杂还有函数定义被编译成了动态链接库。(这里放置一个占位符,等找到教程了之后放在这里),因此我们需要添加链接库:-lsockpp
,不出意外的话,下面的命令可以帮助你成功编译我们的server。
1
2
3
4
5
|
g++ -std=c++17 -o server server.cpp -lsockpp
./server
try accept a conn at port 8080
recv: helloworld # 看下方的telnet可以找到输入
closed
|
- (3)
print_without_crlf
是什么? [了解即可]
CRLF其实是两个字符,回车换行的意思,他们分别是
(CR, ASCII 13, \r)
(LF, ASCII 10, \n)
全称是 Carriage-Return Line-Feed ,可以代表回车的东西,这里有一个Stack Overflow的提问可以帮助你思考这个东西。
如果到这里你不知道ptr是个指针,*ptr会取指针的值,那么你应该先去了解一下有关指针的内容。(占位符,我之后会自己写一篇)。
这串代码,编译成功后按照预期运行。我们即可尝试连接他,并且发送消息。在linux和macos下(好像window也有,只是默认隐藏),有一个小工具叫做 telnet
。这个工具在我们前期的开发中帮助很大,建议找出来并且使用,或者搜索下载一个类似的网络测试助手,可以发起TCP链接并发送数据的小公举。
1
2
3
4
5
6
|
telnet 0.0.0.0 8080
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
hello world # <- 发出去的数据
hello world # <- 收到的数据
|
0x3 使用Cmake进行自动编译Server
我们在Cmake中添加一个可执行文件,向之前的FLAG添加一个-lsockpp
1
2
3
|
set(CMAKE_CXX_FLAGS -g -W -lsockpp)
...
add_executable(server server.cpp)
|
然后我们立刻执行
1
2
3
4
5
6
7
8
9
10
11
12
|
cd build
cmake .. # 每次CMakeLists.txt变化后需要重新执行
make
# 你有可能编译成功,有可能失败(在笔者这里失败了)
make
Scanning dependencies of target server
[ 25%] Building CXX object CMakeFiles/server.dir/server.cpp.o
clang: warning: -lsockpp: 'linker' input unused [-Wunused-command-line-argument]
/Users/dashjay/CLionProjects/http_demo/server.cpp:1:10: fatal error:
'sockpp/tcp_acceptor.h' file not found
#include "sockpp/tcp_acceptor.h"
|
推测是cmake并没有使用系统的的include目录和lib链接目录,导致的,搜索文档后手动添加这两个目录,在笔者电脑上这两个目录分别是/usr/local/include
和/usr/local/lib
,修改后的CMake文件像这样。(已经去掉注释了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
cmake_minimum_required(VERSION 3.16)
set(project_name http_demo)
project(${project_name})
set(CMAKE_CXX_COMPILER "c++")
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS -g -W -lsockpp)
string(REPLACE ";" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
message(STATUS "CMAKE_CXX_FLAGS: " "${CMAKE_CXX_FLAGS}")
include_directories(/usr/local/include)
link_directories(/usr/local/lib)
add_executable(main main.cpp)
add_executable(server server.cpp)
|
再次执行cmake .. && make
后,成功编译出server
,执行,并且使用 telnet
小工具帮助我们调试。
0x4 总结
今天我们做了以下事情:
- 用CPP写了一个Hello World
- 学习使用Cmake运行普通的程序,添加链接库等
- 简单的Sockpp的使用
还有什么问题可以直接到ISSUE中提问,我或者社区里的其他小伙伴会帮你解答的。
[1] Cmake可以帮助跨平台软件的安装和编译,可以通过命令生成一系列Makefile,然后进一步执行编译,测试等指令(对我们来说就这个作用,实际上功能比这个要强大的多。)