03. 简易服务器/客户端
# 03. 简易服务器/客户端
上回书说到套接字编程,这章咱接着唠。咱们要写俩简单的程序(不过这俩程序可不太完整,还有点小毛病),用它们来演示上一章提到的那些系统调用。第一个程序是服务器,它能接收客户端的连接,读取一条消息,再回一条消息。第二个程序是客户端,它负责连接服务器,发一条消息,再读一条消息。咱先从服务器这边开始捣鼓。
首先,得搞到一个套接字文件描述符(fd):
int fd = socket(AF_INET, SOCK_STREAM, 0);
这里的AF_INET
表示使用IPv4,如果想用IPv6或者双栈套接字,那就得用AF_INET6
。为了简单起见,这本书里咱们就只用AF_INET
啦。
SOCK_STREAM
表示使用TCP协议。在这本书里,咱就只用TCP,别的协议先不考虑。这里socket()
调用的三个参数在本书中都是固定的。接下来,再给大家介绍一个新的系统调用:
int val = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
2
setsockopt()
这个调用主要用来配置套接字的各种属性。上面这段代码启用了SO_REUSEADDR
这个选项。要是没有这个选项,服务器重启的时候,可能就没办法绑定到同一个地址啦。这里给大家留个小作业:去搞清楚SO_REUSEADDR
到底是干啥的,为啥要用它。
接下来就是bind()
和listen()
这俩步骤啦,咱们把服务器绑定到通配地址0.0.0.0
的1234
端口上:
// bind, 这是处理IPv4地址的语法
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = ntohs(1234);
addr.sin_addr.s_addr = ntohl(0); // 通配地址0.0.0.0
int rv = bind(fd, (const sockaddr * )&addr, sizeof(addr));
if (rv) {
die("bind()");
}
2
3
4
5
6
7
8
9
// listen
rv = listen(fd, SOMAXCONN);
if (rv) {
die("listen()");
}
2
3
4
5
然后进入循环,处理每一个连接:
while (true) {
// accept
struct sockaddr_in client_addr = {};
socklen_t socklen = sizeof(client_addr);
int connfd = accept(fd, (struct sockaddr * )&client_addr, &socklen);
if (connfd < 0) {
continue; // 出错了,跳过这次循环
}
do_something(connfd);
close(connfd);
}
2
3
4
5
6
7
8
9
10
11
12
do_something()
函数的功能很简单,就是读取和写入数据:
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));
}
2
3
4
5
6
7
8
9
10
11
12
注意哈,read()
和write()
调用会返回读取或写入的字节数。真正的程序员肯定得处理这些函数的返回值,不过在这章里,为了简洁,我省略了好多东西。而且说实话,这章里的代码在实际的网络编程里,也不是啥“标准答案”。
客户端的程序就简单多啦:
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
用下面的命令来编译咱们的程序:
g++ -Wall -Wextra -O2 -g 03_server.cpp -o server
g++ -Wall -Wextra -O2 -g 03_client.cpp -o client
2
先在一个窗口里运行./server
,然后在另一个窗口里运行./client
。你应该能看到这样的结果:
$ ./server
client says: hello
2
$ ./client
server says: world
2
给大家布置个小任务:去看看这章用到的API的手册,或者在网上找些相关教程。一定要学会怎么查找API的使用方法,因为这本书不会详细介绍这些API的具体用法。
03_client.cpp
03_server.cpp