CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 从零用C语言写一个Redis 引言
  • 02. 套接字入门
  • 03. 简易服务器/客户端
    • 03. 简易服务器/客户端
  • 04. 协议解析
  • 05. 事件循环与非阻塞I/O
  • 06. 事件循环的实现
  • 07. 基础服务器:实现get、set、del功能
  • 08. 数据结构:哈希表
  • 09. 数据序列化
  • 10. AVL树:实现与测试
  • 11. AVL树和有序集合
  • 12. 事件循环和定时器
  • 13. 堆数据结构和生存时间(TTL)
  • 14. 线程池与异步任务
目录

03. 简易服务器/客户端

# 03. 简易服务器/客户端

上回书说到套接字编程,这章咱接着唠。咱们要写俩简单的程序(不过这俩程序可不太完整,还有点小毛病),用它们来演示上一章提到的那些系统调用。第一个程序是服务器,它能接收客户端的连接,读取一条消息,再回一条消息。第二个程序是客户端,它负责连接服务器,发一条消息,再读一条消息。咱先从服务器这边开始捣鼓。

首先,得搞到一个套接字文件描述符(fd):

int  fd  =  socket(AF_INET,  SOCK_STREAM,  0);
1

这里的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));
1
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()");
}
1
2
3
4
5
6
7
8
9
//  listen
rv =  listen(fd,  SOMAXCONN);
if  (rv)  {
    die("listen()");
}
1
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);
}
1
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));
}
1
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);
1
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
1
2

先在一个窗口里运行./server,然后在另一个窗口里运行./client。你应该能看到这样的结果:

$ ./server
client says: hello
1
2
$ ./client
server says: world
1
2

给大家布置个小任务:去看看这章用到的API的手册,或者在网上找些相关教程。一定要学会怎么查找API的使用方法,因为这本书不会详细介绍这些API的具体用法。

  • 03_client.cpp
  • 03_server.cpp
上次更新: 2025/03/25, 00:48:42
02. 套接字入门
04. 协议解析

← 02. 套接字入门 04. 协议解析→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式