最近在学习嵌入式开发,练习C语言小项目,基本是参考别人的代码,做了些修改实现了聊天室,纯C语言编写。
想直接看源码的跳到最后。
一、练习内容
- socket通信流程
- 管道的使用
- epoll的使用
首先要熟悉几个结构体,不然对于新手是比较懵圈的,尤其对于我这种习惯了JAVA的菜鸟。
sockaddr和sockaddr_in
struct sockaddr和struct sockaddr_in这两个结构体用来处理网络通信的地址。
在各种系统调用或者函数中,只要和网络地址打交道,就得用到这两个结构体。
struct sockaddr { unsigned short sa_family; // 2 bytes address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address }; // IPv4 AF_INET sockets: struct sockaddr_in { short sin_family; // 2 bytes e.g. AF_INET, AF_INET6 unsigned short sin_port; // 2 bytes e.g. htons(3490) struct in_addr sin_addr; // 4 bytes see struct in_addr, below char sin_zero[8]; // 8 bytes zero this if you want to }; struct in_addr { unsigned long s_addr; // 4 bytes load with inet_pton() };
如何仔细看的话sockaddr和sockaddr_in这两个结构体大小是一样的,区别是sockaddr_in把IP和PORT,协议等字段划分出来,这样方便我们去填充数据,那sockaddr干啥用呢,因为它资历老,一般的socket通信接口只接收sockaddr格式的结构体,所以要传参的时候强制转换成sockaddr,当然如果你就使用sockaddr来填充数据也可以,无非麻烦一点。
所以sockaddr_in方便填充数据,sockaddr用来会传参,这就是二者的作用。
网络字节序转换
htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"
之所以需要这些函数是因为计算机数据表示存在两种字节顺序:NBO与HBO
网络字节顺序NBO(Network Byte Order):
按从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。
主机字节顺序(HBO,Host Byte Order):
不同的机器HBO不相同,与CPU设计有关,数据的顺序是由cpu决定的,而与操作系统无关。
如 Intelx86结构下,short型数0x1234表示为34 12, int型数0x12345678表示为78 56 34 12如IBM power PC结构下,short型数0x1234表示为12 34, int型数0x12345678表示为12 34 56 78
由于这个原因不同体系结构的机器之间无法通信,所以要转换成一种约定的数序,也就是网络字节顺序,其实就是如同powerpc那样的顺序 。在PC开发中有ntohl和htonl函数可以用来进行网络字节和主机字节的转换。
fcntl
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
epoll
epoll是linux2.6内核的一个新的系统调用,epoll在设计之初,就是为了替代select, poll线性复杂度的模型,epoll的时间复杂度为O(1), 也就意味着,epoll> 在高并发场景,随着文件描述符的增长,有良好的可扩展性.
这个系统调用用于操作epoll函数所生成的实例(该实例由epfd指向),向fd实施op操作
EPOLL_CTL_ADD //注册新的fd到epfd中;
EPOLL_CTL_MOD //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL //从epfd中删除一个fd;
events成员变量:
可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
fork
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值
在执行函数fork()时,创建了一个子进程,此时是两个进程同时运行。fork()返回两次,子进程返回值为0
pipe
函数原型 int pipe(int fd[2])
函数传入值 fd[2]:管道的两个文件描述符,之后就是可以直接操作者两个文件描述符管道两端可 分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另 一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O 函数都可以用于管道,如close、read、write等等.
能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文
件,它不属于某种文件系统,而是自立门户,单独构成一种文件系 统,并且只存在与内存中
示例代码说明
源码文件为:
utility.h
client.c
server.c
使用
在linux下进行编译使用,因为源码示例SERVER_IP设置是为本机IP,所以要在同一个机器上运行。在源码目录下执行:
//编译 gcc -o server server.c gcc -o client client.c //运行 ./server ./client //需要另起客户端,client可以启动多个
运行示例,启动了两个客户端:
服务端:
image.png
客户端一:
image.png
客户端二:
image.png