C/C++工程实践训练营 第02-03课
C/C++ 开发编码时的防御和习惯优于调试和生产问题排查。
项目
如何学好C/C++?
工作三年:
-
学习C/C++基本语法
-
学习操作系统常用的API函数以及其背后的操作系统原理
- Windows程序设计(第五版)+ Windows核心编程
- Linux系统编程
std::thread t(threadfunc)
windows----调用函数----CreateThread
linux-----调用函数-----pthread_create
t.join();
windows----调用函数----WaitForSingleObject
linux-----调用函数-----pthread_join
std::fstream
fopen
fread
fwrite
windows----调用函数----
CreateFile
ReadFile
WriteFile
linux-----调用函数-----
open
read
write
std::mutex
std::shared_mutex
std::condition_variable
std::semaphore
工作五年:
- 掌握一些常见的设计模式、设计思想(包括C++常用的设计模式)
- 整个架构(三层结构)
smallchat项目的组织结构
服务端 smallchat-server
数组越界问题
char currentDir[256];
getcwd(currentDir);
currentDir[255] = 0; //截断
std::string strCurrentDir(currentDir);
string(const char* s,size_t n);
string(size_t n,char c);
char c = -1;
size_t len = 65;
std::string str(c, len);//构造函数问题
//A
-
程序结构
//初始化 //一个while循环,在while循环不断检测包括监听fd在内的读事件,如果是监听fd,则接受连接,并将接受的fd挂到select函数,如果是普通fd,处理读事件(收数据)
一个连接 - fd - client 对象
管理所有 clients 的对象 —— struct chatState *Chat
客户端 smallchat-client
程序结构
//初始化(选择一个本机可用ip地址,并通过用户传进来的端口号建立连接)
//使用一个while循环,将connectfd和stdin-fd挂载到select函数上,判断读事件,如果是connectfd的读事件,则从服务端收取数据,如果是stdin-fd读事件,则从控制台收集数据。
好的思路:把stdin-fd也挂载到select函数上,实现了单线程无阻塞处理网络事件和控制台输入。
知识点:select函数的用法
select只是IO复用的一种方式,其他的还有:poll,epoll等。
select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。
我们使用select来监视文件描述符时,要向内核传递的信息包括: 1、我们要监视的文件描述符个数 2、每个文件描述符,我们可以监视它的一种或多种状态,包括:可读,可写,发生异常三种。 3、要等待的时间,监视是一个过程,我们希望内核监视多长时间,然后返回给我们监视结果呢? 4、监视结果包括:准备好了的文件描述符个数,对于读,写,异常,分别是哪儿个文件描述符准备好了。
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
原理 理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。 执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) 若再加入fd=2,fd=1,则set变为0001,0011 执行select(6,&set,0,0,0)阻塞等待 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
select函数的第一个参数
nfds:是一个整数值, 表示集合中所有文件描述符的范围,即所有文件描述符的最大值+1。在windows中不需要管这个。 待测试的描述集总是从0, 1, 2, …开始的,所以要加1。
select函数的第二三四参数
**fd_set:**一个文件描述符集合保存在fd_set变量中,可读,可写,异常这三个描述符集合需要使用三个变量来保存,分别是 readfds,writefds,exceptfds。我们可以认为一个fd_set变量是由很多个二进制构成的数组,每一位表示一个文件描述符是否需要监视。
对于fd_set类型的变量,我们只能使用相关的函数来操作。
void FD_CLR(int fd, fd_set *set);//清除某一个被监视的文件描述符。
int FD_ISSET(int fd, fd_set *set);//测试一个文件描述符是否是集合中的一员
void FD_SET(int fd, fd_set *set);//添加一个文件描述符,将set中的某一位设置成1;
void FD_ZERO(fd_set *set);//清空集合中的文件描述符,将每一位都设置为0;
使用案例:
fd_set readfds;
int fd;
FD_ZERO(&readfds)//新定义的变量要清空一下。相当于初始化。
FD_SET(fd,&readfds);//把文件描述符fd加入到readfds中。
//select 返回
if(FD_ISSET(fd,&readset))//判断是否成功监视
{
//dosomething
}
readfds: 监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者更准确的说,读取是不是不阻塞了。 writefds: 监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写,或者更准确的说,写入是不是不阻塞了。 exceptfds: 用来监视发生错误异常文件
select函数的第五个参数
timeout
struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
}
timeout表示select返回之前的时间上限。 如果timeout==NULL,无期限等待下去,这个等待可以被一个信号中断,只有当一个描述符准备好,或者捕获到一个信号时函数才会返回。如果是捕获到信号,select返回-1,并将变量errno设置成EINTR。
如果timeout->tv_sec= =0 && timeout->= =tv_sec0 ,不等待直接返回,加入的描述符都会被测试,并且返回满足要求的描述符个数,这种方法通过轮询,无阻塞地获得了多个文件描述符状态。
如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的时间。当有描述符复合条件或者超过超时时间的话,函数返回。等待总是会被信号中断。
select函数的返回值
返回值 成功时:返回三中描述符集合中”准备好了“的文件描述符数量。 超时:返回0 错误:返回-1,并设置 errno
EBADF:集合中包含无效的文件描述符。(文件描述符已经关闭了,或者文件描述符上已经有错误了)。 EINTR:捕获到一个信号。 EINVAL:nfds是负的或者timeout中包含的值无效。 ENOMEM:无法为内部表分配内存。
-
FD_ZERO、FD_SET_FD_ISSET
这是几个常用的宏
总结
select()可以同时监视多个描述符,如果他们没有活动,则正确地将进程置于休眠状态。Unix程序员们经常要处理多个文件描述符的I/O,他们的数据流可能是间歇性的。如果只创建read或者write会导致程序阻塞。
在我们使用select的时候,需要注意:
1、我们应该总是设置timeout=0,因为如果没有可用的数据,程序在运行时间里将无视可做。依赖超时的代码通常是不可移植,并且很难调试。
2、nfds的值一要准备且适当。
3、如果在调用完select之后,你不想检查结果,也不想做出适当的响应,那么文件描述符不需要添加到集合中。
4、select返回后,所有的文件描述符都应该被检查,看看他们是否准备好了。
5、read,recv,write,send,这几个函数不一定读/写你所请求的全部数据。如果他们读/写全部数据,是因为低流量负载和快速流。情况并非重视如此,应该处理你的函数仅管理发送或接收单个字节的情况。
6、除非你真的确信你有少量的数据要处理,否则不要一次只读一个字节,当你每次都能缓冲的时候,尽可能多的读取数据是非常低效的。
7、read,recv,write,send和select都会有返回-1的情况,并set errno的值。这些errno必须被恰当的处理。如果你的程序不会接收到任何信号,那么errno永远都不会等于EINTR,如果你的程序并不会设置非阻塞IO,那么errno就不会等于EAGAIN。
8、调用read,recv,write,send,不要使buffer的长度为0;
9、如果read,recv,write,send调用失败,并且返回的errno不是7中说的那两种情况,或者返回0,意思是“end-of-file”,这种情况下我们不应再将文件描述符传递给select。
10、每次调用select之前,timeout都用重新设置。
11、由于select()修改其文件描述符集,如果调用在循环中使用,则必须在每次调用之前重新初始化这些集。
大多数的操作系统都支持select。相比于试图用线程,进程,IPCS,信号,内存共享等方式来解决问题,select函数更有效且轻松。系统调用poll和select相似,在监视稀疏文件集合的时候更加有效。poll现在也在被广泛的使用,但没有select简便。linux专用的epoll在监视大量数据时比select和poll更加有效。
案例
案例1
下面是"man select "帮助文档中案例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
fd_set rfds;//定义一个能保存文件描述符集合的变量
struct timeval tv;//定义超时时间
int retval;//保存返回值
/* Watch stdin (fd 0) to see when it has input. */
/* 监测标准输入流(fd=0)看什么时候又输入*/
FD_ZERO(&rfds);//初始化集合
FD_SET(0, &rfds);//把文件描述符0加入到监测集合中。
/* Wait up to five seconds. */
/* 设置超时时间为5s */
tv.tv_sec = 5;
tv.tv_usec = 0;
/*调用select函数,将文件描述符集合设置成读取监测 */
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
/* 这时候的tv值是不可依赖的 */
/*根据返回值类型判断select函数 */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
/* 因为值增加了一个fd,如果返回值>0,则说明fd=0在集合中。*/
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
案例2
下面是"man select_tut "帮助文档中案例:
这个例子更好的说明了select函数的作用,这是一个TCP转发相关的程序,从一个端口转发到另一个端口
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
static int forward_port;
#undef max
#define max(x,y) ((x) > (y) ? (x) : (y))
static int listen_socket(int listen_port)
{
struct sockaddr_in a;
int s;
int yes;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
return -1;
}
yes = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
(char *) &yes, sizeof(yes)) == -1) {
perror("setsockopt");
close(s);
return -1;
}
memset(&a, 0, sizeof(a));
a.sin_port = htons(listen_port);
a.sin_family = AF_INET;
if (bind(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
perror("bind");
close(s);
return -1;
}
printf("accepting connections on port %d\n", listen_port);
listen(s, 10);
return s;
}
static int connect_socket(int connect_port, char *address)
{
struct sockaddr_in a;
int s;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
close(s);
return -1;
}
memset(&a, 0, sizeof(a));
a.sin_port = htons(connect_port);
a.sin_family = AF_INET;
if (!inet_aton(address, (struct in_addr *) &a.sin_addr.s_addr)) {
perror("bad IP address format");
close(s);
return -1;
}
if (connect(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
perror("connect()");
shutdown(s, SHUT_RDWR);
close(s);
return -1;
}
return s;
}
#define SHUT_FD1 do { \
if (fd1 >= 0) { \
shutdown(fd1, SHUT_RDWR); \
close(fd1); \
fd1 = -1; \
} \
} while (0)
#define SHUT_FD2 do { \
if (fd2 >= 0) { \
shutdown(fd2, SHUT_RDWR); \
close(fd2); \
fd2 = -1; \
} \
} while (0)
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int h;
int fd1 = -1, fd2 = -1;
char buf1[BUF_SIZE], buf2[BUF_SIZE];
int buf1_avail, buf1_written;
int buf2_avail, buf2_written;
//我们希望调用主函数的时候,要指明,本地端口,发送端口,还有发送的ip地址
if (argc != 4) {
fprintf(stderr, "Usage\n\tfwd <listen-port> "
"<forward-to-port> <forward-to-ip-address>\n");
exit(EXIT_FAILURE);
}
// 忽略SIGPIPE这个信号,这个信号常出现在网络编程中,访问一个已经关闭的文件描述符时候出现。
signal(SIGPIPE, SIG_IGN);
//确定发送端口
forward_port = atoi(argv[2]);
//监听本地端口
h = listen_socket(atoi(argv[1]));
if (h == -1)
exit(EXIT_FAILURE);
for (;;) {
int r, nfds = 0;
fd_set rd, wr, er;
FD_ZERO(&rd);
FD_ZERO(&wr);
FD_ZERO(&er);
FD_SET(h, &rd);
// 获取nfds的值。并把fd1,fd2分别加入到,可读,可写,异常监视集合中去。
nfds = max(nfds, h);
if (fd1 > 0 && buf1_avail < BUF_SIZE) {
FD_SET(fd1, &rd);
nfds = max(nfds, fd1);
}
if (fd2 > 0 && buf2_avail < BUF_SIZE) {
FD_SET(fd2, &rd);
nfds = max(nfds, fd2);
}
if (fd1 > 0 && buf2_avail - buf2_written > 0) {
FD_SET(fd1, &wr);
nfds = max(nfds, fd1);
}
if (fd2 > 0 && buf1_avail - buf1_written > 0) {
FD_SET(fd2, &wr);
nfds = max(nfds, fd2);
}
if (fd1 > 0) {
FD_SET(fd1, &er);
nfds = max(nfds, fd1);
}
if (fd2 > 0) {
FD_SET(fd2, &er);
nfds = max(nfds, fd2);
}
//开始监视
r = select(nfds + 1, &rd, &wr, &er, NULL);
if (r == -1 && errno == EINTR)
continue;
if (r == -1) {
perror("select()");
exit(EXIT_FAILURE);
}
if (FD_ISSET(h, &rd)) {
unsigned int l;
struct sockaddr_in client_address;
memset(&client_address, 0, l = sizeof(client_address));
r = accept(h, (struct sockaddr *) &client_address, &l);
if (r == -1) {
perror("accept()");
} else {
SHUT_FD1;
SHUT_FD2;
buf1_avail = buf1_written = 0;
buf2_avail = buf2_written = 0;
fd1 = r;
fd2 = connect_socket(forward_port, argv[3]);
if (fd2 == -1)
SHUT_FD1;
else
printf("connect from %s\n",
inet_ntoa(client_address.sin_addr));
}
}
/* NB: read oob data before normal reads */
if (fd1 > 0)
if (FD_ISSET(fd1, &er)) {
char c;
r = recv(fd1, &c, 1, MSG_OOB);
if (r < 1)
SHUT_FD1;
else
send(fd2, &c, 1, MSG_OOB);
}
if (fd2 > 0)
if (FD_ISSET(fd2, &er)) {
char c;
r = recv(fd2, &c, 1, MSG_OOB);
if (r < 1)
SHUT_FD2;
else
send(fd1, &c, 1, MSG_OOB);
}
if (fd1 > 0)
if (FD_ISSET(fd1, &rd)) {
r = read(fd1, buf1 + buf1_avail,
BUF_SIZE - buf1_avail);
if (r < 1)
SHUT_FD1;
else
buf1_avail += r;
}
if (fd2 > 0)
if (FD_ISSET(fd2, &rd)) {
r = read(fd2, buf2 + buf2_avail,
BUF_SIZE - buf2_avail);
if (r < 1)
SHUT_FD2;
else
buf2_avail += r;
}
if (fd1 > 0)
if (FD_ISSET(fd1, &wr)) {
r = write(fd1, buf2 + buf2_written,
buf2_avail - buf2_written);
if (r < 1)
SHUT_FD1;
else
buf2_written += r;
}
if (fd2 > 0)
if (FD_ISSET(fd2, &wr)) {
r = write(fd2, buf1 + buf1_written,
buf1_avail - buf1_written);
if (r < 1)
SHUT_FD2;
else
buf1_written += r;
}
/* check if write data has caught read data */
if (buf1_written == buf1_avail)
buf1_written = buf1_avail = 0;
if (buf2_written == buf2_avail)
buf2_written = buf2_avail = 0;
/* one side has closed the connection, keep
writing to the other side until empty */
if (fd1 < 0 && buf1_avail - buf1_written == 0)
SHUT_FD2;
if (fd2 < 0 && buf2_avail - buf2_written == 0)
SHUT_FD1;
}
exit(EXIT_SUCCESS);
}