用C/C++构建自己的Redis——第一章、TCP Server和Client
文章目录
前言
数据结构,很多初学者对它的实际用处了解较少,最近看了一本书《Build Your Own Redis with C/C++》,它讲述了如何从0使用C/C++,运用基本的数据结构,构建属于自己的Redis。今天,我们一起学习TCP Server和Client这一部分。
原书链接:https://build-your-own.org/redis/
一、什么是Socket
Socket 是两个实体通过网络进行通信的通道。通过Socket,实现Client给Server发送请求,Server发送响应。
二、创建TCP Server
1.获取Socket Handle
int fd = scoket(AF_INET, SOCK_STREAM, 0);
其中AF_INET表示使用IPv4,SOCK_STREAM表示使用面向连接的TCP协议。
2.配置Socket
有许多选项可以改变Socket的行为。例如TCP生命周期等。
然而,socket()系统调用并不能传递这些选项,因此需要使用另一个系统调用setsockopt()对其进行配置。
int val = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
SOL_SOCKET意思为正在使用的socket选项。SO_REUSEADDR表示是否允许重用地址。
3.绑定到地址
// bind, this is the syntax that deals with IPv4 addresses
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = ntohs(1234);
addr.sin_addr.s_addr = ntohl(0); // wildcard address 0.0.0.0
int rv = bind(fd, (const sockaddr *)&addr, sizeof(addr));
if (rv) {
die("bind()");
}
struct sockaddr_in用于保存一个IPv4地址和端口。对于IPv6,使用struct sockaddr_in6。ntohs() 和 ntohl() 函数会将数字转换为所需的 big endian 格式。
4.Listen
// listen
rv = listen(fd, SOMAXCONN);
if (rv) {
die("listen()");
}
在执行listen()系统调用后,操作系统会自动处理TCP握手,并将已建立的连接放入队列。然后,应用程序就可以通过accept()提取连接。SOMAXCONN参数就是队列的大小。在Linux上,其被定义为128。
5.接受连接
Server进入循环,接受并处理每个client的连接。
while (true) {
// accept
struct sockaddr_in client_addr = {};
socklen_t addrlen = sizeof(client_addr);
int connfd = accept(fd, (struct sockaddr *)&client_addr, &addrlen);
if (connfd < 0) {
continue; // error
}
do_something(connfd);
close(connfd);
}
accept()系统调用会返回地址。addrlen既是输入大小,也是输出大小。
6.读和写
我们只处一个read()和write()
static void do_something(int connfd) {
char rbuf[64] = {};
ssize_t n = read(connfd, rbuf, sizeof(rbuf) - 1);
if (n < 0) {
msg("read() error");
return;
}
printf("client says: %s\n", rbuf);
char wbuf[] = "world";
write(connfd, wbuf, strlen(wbuf));
}
read()和write()返回的都是字节数,这里我们只演示如何使用Socket的API,不进行细致编程。
三、创建TCP Client
Client要做的是:写东西,从Server读回,然后关闭连接
1.连接
这里没什么好说的,直接看代码吧。
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
die("socket()");
}
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = ntohs(1234);
addr.sin_addr.s_addr = ntohl(INADDR_LOOPBACK); // 127.0.0.1
int rv = connect(fd, (const struct sockaddr *)&addr, sizeof(addr));
if (rv) {
die("connect");
}
char msg[] = "hello";
write(fd, msg, strlen(msg));
char rbuf[64] = {};
ssize_t n = read(fd, rbuf, sizeof(rbuf) - 1);
if (n < 0) {
die("read");
}
printf("server says: %s\n", rbuf);
close(fd);
2.编译和运行
编译:
g++ -Wall -Wextra -O2 -g 03_server.cpp -o server
g++ -Wall -Wextra -O2 -g 03_client.cpp -o client
运行:
$ ./server
client says: hello
$ ./client
server says: world
四、其中用到的结构体定义
struct sockaddr {
unsigned short sa_family; // AF_INET, AF_INET6
char sa_data[14]; // useless
};
struct sockaddr_in {
short sin_family; // AF_INET
unsigned short sin_port; // port number, big endian
struct in_addr sin_addr; // IPv4 address
char sin_zero[8]; // useless
};
struct sockaddr_in6 {
uint16_t sin6_family; // AF_INET6
uint16_t sin6_port; // port number, big endian
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr; // IPv6 address
uint32_t sin6_scope_id;
};
struct sockaddr_storage {
sa_family_t ss_family; // AF_INET, AF_INET6
// enough space for both IPv4 and IPv6
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
总结
这一章主要了解Client-Server的通信过程,代码总和如下:
client.cpp:https://build-your-own.org/redis/03/03_client.cpp.htm
server.cpp:https://build-your-own.org/redis/03/03_server.cpp.htm