Linux使用c++我们会使g++来编码,使用gdb工具进行代码调试。同时在大型项目中,我们编写makefile来自动化编译。
知识点
- 使用 g++ 编译器编译代码
- 使用 gdb 进行调试
- 编写 makefile
首先明确c++编译过程分为四个阶段:预处理 编译 汇编 链接
预处理负责 宏定义的替换、条件编译、将include头文件展开到正文。
编译负责将源码转换成汇编代码。
汇编负责将汇编代码转成可重定位的目标二进制文件。
链接 负责将所有的目标文件连接起来,进行符号解析和重定位,最后生成可执行文件。
g++ test.cpp
安装 gdb
1.
wget http://ftp.gnu.org/gnu/gdb/gdb-8.0.1.tar.gz tar -zxvf gdb-8.0.1.tar.gz cd gdb-8.0.1/ ./configure make sudo make install gdb -v
2.
sudo apt install gdb
编译时要加入-g 参数
为了能使用 gdb 调试,我们在用 g++ 编译时要将 -g 参数加上,如:
g++ -g -o test a.cpp b.cpp
gdb 常用调试命令
以前面“多文件编译”的程序为例,我们可使用如下命令进入 gdb 调试:
gdb test
常用的调试命令有: l:查看代码 b 5:在程序的第 5 行添加断点 info break:查看断点 r:开始运行 s:进入函数内部 n:进入下一步 finish:跳出函数内部 c:运行到下一个断点
编写 makefile 进行自动编译
在大型项目中有大量的源代码文件,我们不可能每次都逐个敲 g++ 命令来进行编译,而是采用编写 makefile 的方式来进行自动编译,提高效率。
和创建源代码文件一样,我们可以直接用 vi 编辑器来创建 makefile,如下:
vi makefile
makefile 的一般格式如下:
目标名1:依赖文件1,依赖文件2,依赖文件3
g++ 编译命令
目标名2:依赖文件4,依赖文件5
g++ 编译命令
其中目标名可以由自己定义,也可以是一个文件的名字;依赖文件就是说要达成这个目标所需要的文件。 仍以前面的“多文件编译”代码为例,我们可以写出如下的 makefile 文件(注意:makefile 文件中要使用 tab 键,不能使用空格键,否则会报错):
target:a.cpp b.o
g++ -o test a.cpp b.o
b.o:b.cpp
g++ -c b.cpp
可以看出,target 依赖于 a.cpp 和 b.o,而 b.o 依赖于 b.cpp,因此编译时发现 b.cpp 更新了的话就会先执行后面的命令来更新 b.o。
保存好 makefile 文件之后,我们用命令行输入 make 即可进行自动编译:
快速清理目标文件
有时候我们想要删掉 makefile 产生出来的所有目标文件,如果逐个去删显得过于麻烦,因此我们可以借助 make clean。
仍然是在前面的 makefile 文件中修改,在后面补上一个 clean:,以及相应的清除命令:
target:a.cpp b.o
g++ -o test a.cpp b.o
b.o:b.cpp
g++ -c b.cpp
.PHONY:clean
clean:
rm *.o
rm test
这样当我们在命令行执行 make clean 就可删掉所有目标文件。
服务器和客户端进行简单的 TCP 通信
在 TCP/IP 四层参考模型中,从上往下有四种层次:应用层、传输层、网络层、网络接口层。TCP 是传输层中重要的一项协议,也是我们常用的进程间通信方式,因此掌握 C++ 中的 TCP 套接字网络编程十分重要。
知识点
- 计算机网络传输层基础知识
- TCP 套接字网络编程
传输层的作用
传输层的根本目的是在网络层提供的数据通信服务基础上,实现主机的进程间通信的可靠服务。
有以下两个要点:为位于两个主机内部的两个应用进程之间提供通信服务、提供可靠的通信服务。
套接字
“套接字”表示一个 IP 地址与对应的一个端口号。例如,一个 IP 地址为 172.31.75.8 的客户端使用 8050 端口号,那么标识客户端的套接字为“172.31.75.8:8050”。
端口号
端口号为 0-65535 之间的整数,有 3 种类型:熟知端口号、注册端口号、临时端口号。
熟知端口号:给每种服务器分配确定的全局端口号。每个用户进程都知道相应的服务器进程的熟知端口号,范围为 0-1023,它是统一分配和控制的。
注册端口号:在 IANA 注册的端口号,数值范围为 1024-49151。
临时端口号:客户端程序使用临时端口号,它是运行在客户端上的 TCP/IP 软件随机选取的,范围为 49152-65535。
我们平时进行网络编程时服务器最好使用注册端口号,而客户端的端口号则是系统随机分配的,即临时端口号。
UDP 用户数据报协议
UDP 协议主要有以下一些特点:
- 无连接的:发送数据之前不需要建立连接,因此减少了开销和发送数据之前的时延。
- 尽最大努力交付:即不保证可靠交付,因此主机不需要维持复杂的连接状态表。
- 面向报文的:UDP 对应用层传递下来的报文,既不合并,也不拆分,而是保留这些报文的边界。UDP 对于应用程序提交的报文,添加头部后就向下提交给网络层。
- 没有拥塞控制:网络出现的拥塞时,UDP 不会使源主机的发送速率降低。这对某些实时应用是很重要的,很适合多媒体通信的要求。
- 支持多对多的交互通信
UDP 的适用场景:
- 适用于少量(几百个字节)的数据。
- 对性能的要求高于对数据完整性的要求,如视频播放、P2P、DNS 等。
- 需要“简短快捷”的数据交换 简单的请求与应答报文交互,如在线游戏。
- 需要多播和广播的应用,源主机以恒定速率发送报文,拥塞发生时允许丢弃部分报文,如本地广播、隧道 VPN。
TCP 传输控制协议
用 C++ 进行 TCP 套接字网络编程
网络编程中我们一般会使用 C/S 架构,即包含服务器端和客户端。
TCP 网络编程的服务器端
服务器端一般先用 socket 创建一个套接字,然后用 bind 给这个套接字绑定地址(即 ip+端口号),然后调用 listen 把这个套接字置为监听状态,随后调用 accept 函数从已完成连接队列中取出成功建立连接的套接字,以后就在这个新的套接字上调用 send、recv 来发送数据、接收数据,最后调用 close 来断开连接释放资源即可。
过程如下:
TCP 网络编程的客户端
与服务器不同,客户端并不需要 bind 绑定地址,因为端口号是系统自动分配的,而且客户端也不需要设置监听的套接字,因此也不需要 listen。客户端在用 socket 创建套接字后直接调用 connect 向服务器发起连接即可,connect 函数通知 Linux 内核完成 TCP 三次握手连接,最后把连接的结果作为返回值。成功建立连接后我们就可以调用 send 和 recv 来发送数据、接收数据,最后调用 close 来断开连接释放资源。
完整流程
TCP 网络编程的整体流程如下图所示:
TCP 网络编程的相关数据结构
地址结构:
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
上述结构体涉及到的另一个结构体 in_addr 如下:
struct in_addr {
unsigned long s_addr;
};
TCP 网络编程各函数的定义
int socket( int domain, int type,int protocol)
int bind(int sockfd,struct sockaddr* my_addr,int addrlen)
int listen(int sockfd,int backlog)
int accept(int sockfd, structsockaddr *addr, int *addrlen)
int send(int sockfd, const void * data, int data_len, unsigned int flags)
int recv(int sockfd, void *buf, intbuf_len,unsigned int flags)
close(int sockfd)
int connect(int sockfd,structsockaddr *server_addr,int sockaddr_len)
#include#include #include #include #include #include #include #include #include #include #include using namespace std; int main() { //定义sockfd int server_sockfd = socket(AF_INET,SOCK_STREAM, 0); //定义sockaddr_in struct sockaddr_in server_sockaddr; server_sockaddr.sin_family = AF_INET;//TCP/IP协议族 server_sockaddr.sin_port = htons(8023);//端口号 server_sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址,127.0.0.1是环回地址,相当于本机ip //bind,成功返回0,出错返回-1 if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1) { perror("bind");//输出错误原因 exit(1);//结束程序 } //listen,成功返回0,出错返回-1 if(listen(server_sockfd,20) == -1) { perror("listen");//输出错误原因 exit(1);//结束程序 } //客户端套接字 struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr); //成功返回非负描述字,出错返回-1 int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length); if(conn<0) { perror("connect");//输出错误原因 exit(1);//结束程序 } cout<<"客户端成功连接n"; //接收缓冲区 char buffer[1000]; //不断接收数据 while(1) { memset(buffer,0,sizeof(buffer)); int len = recv(conn, buffer, sizeof(buffer),0); //客户端发送exit或者异常结束时,退出 if(strcmp(buffer,"exit")==0 || len<=0) break; cout<<"收到客户端信息:"< #include#include #include #include #include #include #include #include #include #include #include using namespace std; int main() { //定义sockfd int sock_cli = socket(AF_INET,SOCK_STREAM, 0); //定义sockaddr_in struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET;//TCP/IP协议族 servaddr.sin_port = htons(8023); //服务器端口 servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服务器ip //连接服务器,成功返回0,错误返回-1 if (connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("connect"); exit(1); } cout<<"连接服务器成功!n"; char sendbuf[100]; char recvbuf[100]; while (1) { memset(sendbuf, 0, sizeof(sendbuf)); cin>>sendbuf; send(sock_cli, sendbuf, strlen(sendbuf),0); //发送 if(strcmp(sendbuf,"exit")==0) break; } close(sock_cli); return 0; } 多线程客户端
我们前面几次实验所使用的客户端都是单线程的,只具有发送功能,但一个具备聊天功能的客户端应该同时具备发送和接收信息的功能,因此我们需要使用多线程来实现客户端,一个线程可以接收消息并打印,另一个线程可以输入信息并发送
知识点
- 多线程客户端设计
- TCP 套接字网络编程
- C++ 11 的 thread 库使用
- 面向对象程序设计思想
具体要求
把前面实验的客户端升级为多线程客户端(一个线程用于接收并打印信息、一个线程用于输入并发送信息),为前面实验的多线程服务器添加自动回复客户端的代码,用一个终端运行服务器程序,多个终端运行客户端程序,多个客户端都能发送信息送达服务器并收到服务器的应答,并将应答打印到客户端终端上,当用户在客户端输入 exit 时,要结束两个线程之后再结束客户端进程。
- 编写一个客户端类 client ,有发送线程和接收线程,可以同时发送消息和接收消息。
- 要编写多个源代码文件:client 头文件给出 client 类声明、client.cpp 给出类方法具体实现、test_client.cpp 中编写主函数创建 client 实例对象并测试。
- 当用户在客户端输入 exit 时,要结束发送线程和接收线程之后才退出主线程。
- 服务器程序要在实验 3 的基础上进行一定修改,能够回复消息。
- 编写 Makefile 进行自动编译,使用 git 管理版本。
客户端应先 connect 服务器建立连接,成功连接之后就创建发送线程和接收线程,与服务器类的设计同理,我们需要将发送线程和接收线程的函数设为静态成员函数,发送线程和接收线程中都使用 while(1) 的循环结构,循环终止的条件是用户输入了 exit 或者对端关闭了连接。
添加用户注册功能
在前面的实验中,我们已经将服务器和客户端的多线程框架建立好了,接下来可以开始实现具体的业务逻辑,我们在这次实验中将尝试加入用户注册的功能,在客户端为用户提供注册的选项,并将用户注册的账号、密码发送到服务器端,并保存在服务器的 MySQL 数据库中。
知识点
- MySQL 数据库基础知识
- 使用 SQL 语句操作数据库
- C++ 连接数据库并进行操作
数据库一般分为关系型数据库和非关系型数据库,关系型数据库有我们常见的 MySQL、Oracle 等,非关系型数据库有 Redis 等。
特点
关系型数据库是用二维表格模型来组织数据信息的数据库。MySQL 实际上是一个关系数据库管理系统,在这个系统里面可以有多个数据库,每个数据库中可以有多张表,每张表中可以有多列属性和多行数据。
比如我们要添加用户注册功能,就可以创建一个专用的数据库,在数据库中建一张叫 USER 的表,表中包含用户名 NAME 和密码 PASSWORD 两列属性,然后下面的若干行就是具体的数据,如下:
SQL 结构化查询语言
SQL 即结构化查询语言,是一种数据库查询语言,用来管理关系数据库系统,以及对数据进行增删查改等操作。
Linux 下启动 MySQL
我们实验所用的桌面容器已经安装好了 MySQL,因此不需要自己安装。
首先启动 MySQL 服务,如下:
sudo service mysql start
C++连接 MySQL 的环境配置
更新源并安装 dev 组件:
sudo apt update sudo apt install -y libmysqlclient-dev安装完毕之后查看 /usr/include/mysql 目录下,是否有 mysql.h 文件,如果有的话就说明安装成功:
ls /usr/include/mysql|grep mysql.h
要求
在之前实验的服务器客户端代码中增添用户注册的功能,在客户端为用户提供注册的选项,并将用户注册的账号、密码发送到服务器端,并保存在服务器的 MySQL 数据库中。
- 在服务器本地 Mysql 中创建新数据库 ChatProject,库中有一张表叫 USER,该表中有账号 NAME 和密码 PASSWORD 两项属性。
- 为客户端添加注册功能,让用户注册账号密码,客户端将注册信息发送到服务器端,注意:当用户注册时应输入两次密码,如果密码不一致需要重新输入。
- 服务器端接收来自客户端的注册信息,将用户的账号、密码写入本地 Mysql 数据库。
- 要面向对象编程,进行类封装。
具体实现
首先在 MySQL 控制台创建数据库 ChatProject,如下:
create database ChatProject;



