static用法

static修饰局部变量时用来延长生命周期
修饰全局变量或者函数时用来限制作用域
修饰静态成员变量的时候所有的类对象共享此资源
访问此资源时既可以用类访问也可以用对象访问
记得要在类外初始化
static修饰静态成员函数的时候静态成员函数没有this指针也就不能访问非静态资源
Q:什么时候使用静态成员函数
: 当不需要实例化不需要访问非静态资源就有的确定行为
static语句放在public和private中是等效的

c++函数增强部分

c++的函数在声明的时候可以定义默认值参数有几个注意事项
1.当函数的定义和声明分开时默认值参数只能在声明处定义
2.如果某个参数时默认参数那么它后面的参数都必须是默认参数


在同一目录下同时建立a.c和b.c而且同时声明了相同函数名的函数如果用c语言在main.c中想调用这两个文件中的相同函数是很难办到的即使你在某个文件中使用了static关键字表示作用域范围也是很不方便的在C++中的命名空间就很好的解决了这点

Read More

tcp粘包解决方案

tcp是可靠的面向字节流的协议传输过程中可能就会

完整的服务器端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
  
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

#define BACKLOG 10

//./a.out ip port
int main(int argc,char *argv[])
{
int sfd,cfd,ret;
struct sockaddr_in svr_addr,cli_addr;
char buffer[1024] = {0};
ssize_t sbytes = 0,rbytes = 0;
int length;
int total_received;
socklen_t len = sizeof(struct sockaddr_in);
if (argc != 3){
fprintf(stderr,"Usage : %s < ip > < port >.\n",argv[0]) ;
exit(EXIT_FAILURE);
}

//1.创建套接字
sfd = socket(AF_INET,SOCK_STREAM,0);
if (sfd == -1){
perror("[ERROR] Failed to socket.");
exit(EXIT_FAILURE);
}

bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);

//2.绑定ip地址与端口号
ret = bind(sfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr));
if (ret == -1){
perror("[ERROR] Failed to bind.");
exit(EXIT_FAILURE);
}
//3.设置监听套接字为监听状态<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>建立监听队列
ret = listen(sfd,BACKLOG);
if (ret == -1){
perror("[ERROR] Failed to listen.");
exit(EXIT_FAILURE);
}

//4.与客户端进行三次握手<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>并建立连接,默认是阻塞
cfd = accept(sfd,(struct sockaddr *)&cli_addr,&len);
if (ret == -1){
perror("[ERROR] Failed to accpet.");
exit(EXIT_FAILURE);
}

printf("ip : %s port :%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
for(;;){
length = 0;
total_received = 0;
//接收数据的长度
rbytes = recv(cfd,&length,4,0);
if (rbytes == -1){
perror("[ERROR] Failed to recv.");
exit(EXIT_FAILURE);
}
for(;;){

rbytes = recv(cfd,buffer + total_received,length - total_received,0);
if (rbytes == -1){
perror("[ERROR] Failed to recv.");
exit(EXIT_FAILURE);
}else if (rbytes == 0){
printf("The client has been shutdown.\n");
}else if (rbytes > 0){
total_received += rbytes;
if (total_received == length)
break;
}
}
printf("buffer : %s\n",buffer);
sleep(1);
}

close(sfd);
<span class="bd-box"><h-char class="bd bd-beg"><h-inner>。</h-inner></h-char></span>
return 0;
}

中特别要注意rbytes = recv(cfd,buffer + total_received,length - total_received,0);
这段代码的意思就是从cfd中读取数据到buffer+已经接收的数据地址段为什么要length+total_received因为一段数据可能会被拆分发送也就是说如果只填length的话第一次的recv可能只会接收前半段的拆分数据那么后面的数据就接受不到如果像上面正确设置即使第一次没有接受完因为total_received的值和length不等就会在下个循环继续接收接受的字节数就等于length-已经接受的字节数

牢骚

真的不喜欢数据结构啊啊 链表什么的真讨厌啊啊啊啊 made真的恶心各种结构体遍历真的无趣T_T

还是先学着后面的吧udp的局域网聊天先放一放本来想着能实现也是很不错的但是代码也不完整等以后回头了把这块和学生管理系统那块再统一看看T_T

并发多进程服务器网络聊天模型

这两天在想到底是否学会了怎么验证你学会了我一直在纠结但是昨天早上醒来我觉得我可能迄今为止做错了很多事比葫芦画瓢谁都会靠着自己脑中残留的记忆来照着写真的是硬实力吗我不觉得我认为只有自己写出来了才算你牛逼

这两天虽然只写了不多的代码但我觉得让我受益匪浅把遇到的问题即时记录下来则会很有帮助
1.warning的警告要看在哪里警告警告的提示跟函数参数的出入在哪我的问题在于编译时提示我recvfrom的参数有问题

Read More

环形队列数据读写实现

在此项目中创建了一个信号量集合并包含了三个信号量.

使用一个信号量 (SEM_MUTEX) 用于共享内存的互斥.
使用两个信号量SEM_EMPTY 与 SEM_FULL用于环形队列的同步


SEM_EMPTY = 0,表示目前队列只有0个数据为空本人倾向于记SEM_CURRENT=0,更方便记忆

当 SEM_FULL = 0 时,表示目前队列剩余0为满,则生产者进程阻塞本人倾向于记SEM_REST=0,更方便记忆


网络编程之字节序转换API

ip字符串转换网络字节序

1
2
int inet_aton(const char *cp, struct in_addr *inp);     [addr to network]
功能:将cp指向的IP字符串转成网络字节inp保存的地址中

网络字节序转换IP字符串

1
2
3
4
5
6
7
char *inet_ntoa(struct in_addr in);   [network to addr]
功能将IP网络字节序转换成IP字符串
参数:
@in IP网络字节序

返回值:
成功返回IP字符串首地址失败返回NULL

主机字节序转换为网络字节序

1
2
3
4
5
short htons(short data);        [host  to network short ]
功能short类型的整数转成网络字节序
参数
@data 序号转换的整数
返回值得到的网络字节序

网络字节序转换为十进制数

1
2
3
4
5
6
7
8
9
10
11
12
13
int  atoi(const char *nptr)
功能把ntpr 所指向的整数字符串转换成整数
参数
@ nptr 字符串
返回值成功返回转换后的整数
失败返回0
注意若是只有+-和整数字符能转换其他字符返回0

uint32_t ntohs(uint32_t netlong); [network to host short]
功能把网络字节序转换为主机端口
参数
@ netlong 网络字节序
返回值 返回对应的主机端口

进程

所谓进程顾名思义就是描述一个程序的执行过程

进程与程序的区别

  • 程序是存储在磁盘资源中的静态指令集合没有执行的概念
  • 进程是程序的动态过程包括创建凋亡等等

并发与并行的区别

  • 并发是指在同一时间段有多个任务同时执行由操作系统的调度算法来实现比较经典的就是时间片轮转
  • 并行是指在多个处理器核心下同时执行任务比如一个cpu4核心那么可以同时在每个核心中执行任务也就是可以同时执行四个任务

进程的地址空间由内核空间和用户空间组成下面主要介绍用户空间

stack用来存储非静态的局部变量
heap用来存储动态申请的内存
.bss用来存放未初始化的全局变量
.data用来存放初始化过并且值不等于0的全局变量
.rodata用来存放只读的变量
.text用来存放程序的文本段

当用户进程需要通过内核获取资源时,会切换到内核态运行,这时当前进程会使用内核空间的资源


进程的状态

  • 运行态
  • 睡眠态

    可中断的睡眠可被信号唤醒或者等待事件或者资源就绪
    不可中断的睡眠只能等待特定的事件或者资源就绪

  • 停止态(进程暂停接受某种处理例如gdb调试断点信息处理)
  • 僵尸态(进程已经结束但是资源未被释放)

进程之间的通信
进程之间的通信方式有:管道,信号,消息队列,共享内存,网络


1.管道
管道

  • 无名管道
    无名管道用于父子进程之间通讯而且属于单向通讯
    无名管道读端与写端抽象成两个文件进行操作在无名管道创建成功之后则会返回读端与写端的文件描述符.

    创建无名管道
    int pipe(int pipefd[2]);
    特点
    1.当管道为空时读管道会阻塞读进程
    2.当管道的写端被关闭了从管道中读取剩余数据后read 函数返回 0
    3.管道的大小是有限的,不能让父/子进程同时对管道进行读/写操作
  • 有名管道
    有名管道是在 文件系统中可见的文件,但是不占用磁盘空间,仍然在内存中,可以通过 mkfifo 命令创建有名管道
    有名管道用于 任意进程之间的通讯,当管道为空时,读进程会阻塞.
    创建有名管道
    1
    2
    3
    4
    5
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
    //pathname : 有名管道路径名
    //mode : 有名管道文件访问权限
    优缺点
    优点可以实现任意进程间通信操作起来和文件操作一样
    缺点
    1.打开的时候需要读写一起进行否则就会阻塞管道大小是 4096个字节
    2.半双工的工作模式如果和多个进程通信则需要创建多个管道

2.信号
定义

信号是在软件层次上 是一种通知机制,对中断机制的一种模拟是一种异步通信方式

信号的种类
信号的种类有很多在linux下可以根据 kill -l命令查看由于命令很多就不在此列举了常用的一个信号是SIGKILL 该信号用来立即结束程序的运行并且不能被阻塞处理和忽略 它在操作系统中的定义如下:
#define SIGKILL 9
所以我们想杀死进程可以用
kill -9 someprocess
信号的处理流程
1.信号的发送由进程发出
2.信号的投递和处理进程发出的信号由内核接受然后投递给具体的进程处理

如图中所示处理方式有三种

  • 忽略(除SIGKILL及SIGSTOP)
  • 根据自定义处理函数进行处理
  • 执行默认操作

信号的发送
函数
kill()和raise()
信号的接收
函数
pause()//阻塞等待信号
信号处理之用户自定义函数
先了解信号处理时注册到内核的函数
sighandler_t signal(int signum, sighandler_t handler);
其后参数的sighandler_t类型为:
typedef void (*sighandler_t)(int); typedef void (*)(int) sighandler_t;
以下为一个父进程向子进程发信号子进程按照自定义处理的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void do_sig_usr(int sig)
{
printf(" Receive %s \n",strsignal(sig));
}

int main(void)
{
pid_t cpid;

// 设置信号处理方式
if(signal(SIGUSR1,do_sig_usr) == SIG_ERR){
perror("[ERROR] signal(): ");
exit(EXIT_FAILURE);
}

cpid = fork();

if(cpid == -1){
perror("fork(): ");
exit(EXIT_FAILURE);
}else if(cpid == 0){
printf("Child Process < %d > start.\n",getpid());

pause(); // 等待信号唤醒,这里需要信号到达之前进入睡眠状态

exit(EXIT_SUCCESS);
}else if(cpid > 0){

sleep(1); // 延时 1s,让子进程先执行

kill(cpid,SIGUSR1);// 给子进程发送 SIGUSR1 信号

wait(NULL);// 等待子进程退出
}

return 0;
}


子进程退出信号
因为父进程fork子进程后如果用wait阻塞等待子进程的结束就过于占用资源而且极其不方便所以可以改进一下.
我们从子进程的退出事件为着手点因为子进程的退出是异步事件即不影响父进程而且子进程退出后会自动给父进程发送 SIGCHLD 信号
我们就利用这点
想法自定义处理函数do_sig_child对SIGCHLD进行处理处理方法函数内部wait
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>

void do_sig_child(int sig)
{
printf("Receive signal < %s >\n",strsignal(sig));
wait(NULL);
}

int main(void)
{
pid_t cpid;
__sighandler_t sigret;

sigret = signal(SIGCHLD,do_sig_child);
if (sigret == SIG_ERR){
perror("[ERROR] signal(): ");
exit(EXIT_FAILURE);
}

cpid = fork();
if (cpid == -1){
perror("[ERROR] fork(): ");
exit(EXIT_FAILURE);
}else if (cpid == 0){
printf("Child process < %d > start.\n",getpid());
sleep(2);
exit(EXIT_SUCCESS);
}else if (cpid > 0){
while(1){
/*nothing*/
}
}
return 0;
}

静态库与动态库

静态库

在与文件链接形成可执行文件后会绑定在一起所以优点也就是在执行的时候不需要加载可以直接运行缺点相同的就是重复的占用会导致内存开销过大
静态库在linux以.a结尾,在windows中以.lib结尾

静态库的制作

首先需要把file.c编译成file.o:
gcc -c file.c -o file.o
然后把.o文件生成静态库:

1
ar -rs file.a file.o

生成静态库之后我们需要把main.c与静态库链接起来生成可执行文件
gcc -I<头文件路径> -L<库的路径> main.c -l<静态库的名字> -o <可执行文件名>
gcc 编译器默认搜索头文件与库文件的路径
/usr/include 为头文件默认路径
/usr/lib 与 /lib 为库的默认路径.
假如你已经把头文件库拷到了默认路径那么就不需要加上上两个参数


动态库
与静态库相比显然有着不同的特性动态库加载速度偏慢但在一般情况对内存的占用要比静态库更少
在windows中以.dll文件结尾十分常见在linux中以.so文件结尾.
当可执行文件调用动态库中的函数时则需要加载动态库到内存中
动态库的制作:

gcc -c file.c -o file.o

然后生成动态库
gcc -shared file.o -o file.so
链接的命令则和静态库的一样然后我们如果只这样执行可执行文件是会报错的

error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

这是因为我们没有将动态库加载到内存中默认动态库的位置是/usr/lib如果我们的动态库在当下目录我们可以
export LD_LIBRARY_PATH=.