C语言之网络编程(必背知识点)
温馨提示:这篇文章已超过450天没有更新,请注意相关的内容是否还可用!
一、认识网络
1、网络发展史
网络的来历_百度知道
ARPnetA--Internet--移动互联网--物联网
2、局域网和广域网
局域网(LAN)
局域网的缩写是LAN,local area network,顾名思义,是个本地的网络,只能实现小范围短距离的网络通信。我们的家庭网络是典型的局域网。电脑、手机、电视、智能音箱、智能插座都连在路由器上,可以互相通信。局域网,就像是小区里的道路,分支多,连接了很多栋楼。
广域网(Wan)
广域网(Wide Area Network)是相对局域网来讲的,局域网的传输距离比较近,只能是一个小范围的。如果需要长距离的传输,比如某大型企业,总部在北京,分公司在长沙,局域网是无法架设的。广域网,就像是大马路,分支可能少,但类型多,像国道、省道、高速、小道等,连接了很多大的局域网。
这时需要其它的解决方案。
第一,通过因特网,只需要办一根宽带,就实现了通信,非常方便,现在的宽带价格也比较便宜。
第二,通过广域网专线。
所以为了数据安全,不能连接因特网,需要用一条自己的专用线路来传输数据,这条线路上只有自己人,不会有其他人接入,且距离很远,这个网络就叫 “广域网”。
3、光猫
光猫是一种类似于基带modem(数字调制解调器)的设备,和基带modem不同的是接入的是光纤专线,是光信号。用于广域网中光电信号的转换和接口协议的转换,接入路由器,是广域网接入。
将光线插入左侧的灰色口,右侧网口接网线到路由器即可。
4、交换机与路由器
交换机(二层):用于局域网内网的数据转发路由器(三层):用于连接局域网和外网
路由器有交换机的功能,反之不成立,交换机没有IP分配和IP寻址的功能。
交换机各个口是平等的,所有接入的设备需要自己配置IP,然后组成局域网。
路由器需要区分WAN口和LAN口,WAN口是接外网的(从Modem出来的或者从上一级路由器出来的),LAN口是接内网的,现在路由器都带无线功能,本质上无线接入就是LAN。
5、网线
背过一种线序,了解网线的制作流程。
网线线序
网线制作教程
6、IP地址
6.1 基本概念
- IP地址是Internet中主机的标识
- Internet中的主机要与别的机器通信必须具有一个IP地址
- IP地址为32位(IPv4)或者128位(IPv6)
- 表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。
6.2 网络号/主机号
6.2.1 地址划分
主机号的第一个和最后一个都不能被使用,第一个作为网段号,最后一个最为广播地址。
A类:1.0.0.1~126.255.255.254 B类:128.0.0.1~~191.255.255.254 C类:192.0.0.1~~223.255.255.254 D类(组播地址):224.0.0.1~~239.255.255.254
6.2.2 特殊地址
0.0.0.0:在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。
127.0.0.1:回环地址/环路地址,所有发往该类地址的数据包都应该被loop back。
6.3 子网掩码
IP地址=网络号+主机号,使用子网掩码来进行区分
网络号:表示是否在一个网段内(局域网)
主机号:标识在本网段内的ID,同一局域网不能重复
- 子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址;
- 子网掩码长度是和IP地址长度完全一样;
- 网络号全为1,主机号全为0;
- 公式:网络号=IP & MASK
思考一:上图中B类地址的子网掩码怎么写?
思考二:B类地址,同一网段最多可以连接多少个主机?
思考三:已知一个子网掩码号为255.255.255.192,问,最多可以连接多少台主机?
7、网络模型
7.1 网络的体系结构
- 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
- 每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务
- 网络体系结构即指网络的层次结构和每层所使用协议的集合
- 两类非常重要的体系结构:OSI与TCP/IP
7.2 OSI模型
- OSI模型是一个理想化的模型,尚未有完整的实现
- OSI模型共有七层
- OSI现阶段只用作教学和理论研究
7.3 TCP/IP模型
网络接口和物理层:屏蔽硬件差异(驱动),向上层提供统一的操作接口。
网络层:提供端对端的传输,可以理解为通过IP寻址机器。
传输层:决定数据交给机器的哪个任务(进程)去处理,通过端口寻址
应用层:应用协议和应用程序的集合
OSI和TCP/IP模型对应关系图
7.4 常见网络协议
网络接口和物理层: ppp:拨号协议(老式电话线上网方式) ARP:地址解析协议 IP-->MAC RARP:反向地址转换协议 MAC-->IP 网络层: IP(IPV4/IPV6):网间互连的协议 ICMP:网络控制管理协议,ping命令使用 IGMP:网络分组管理协议,广播和组播使用 传输层: TCP:传输控制协议 UDP:用户数据报协议 应用层: SSH:加密协议 telnet:远程登录协议 FTP:文件传输协议 HTTP:超文本传输协议 DNS:地址解析协议 SMTP/POP3:邮件传输协议
注意:TCP和IP是属于不同协议栈层的,只是这两个协议属于协议族里最重要的协议,所以协议栈或者模型以之命名了。
8. TCP/UDP
TCP
TCP(即传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。
适用场景
适合于对传输质量要求较高的通信
在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
UDP
UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用场景
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
适合于广播/组播式通信中。
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
9. 编程预备知识
9.1 socket定义
9.2 socket类型
流式套接字(SOCK_STREAM) TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字(SOCK_DGRAM) UDP
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问。
9.4 端口号
- 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区
- TCP端口号与UDP端口号独立
- 端口号一般由IANA (Internet Assigned Numbers Authority) 管理
- 端口用两个字节来表示
众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用) 已登记端口:1024~49151 动态或私有端口:49152~65535
9.5 字节序
小端序(little-endian) - 低序字节存储在低地址
大端序(big-endian)- 高序字节存储在低地址
网络中传输的数据必须使用网络字节序,即大端字节序
面试题:写一个函数,判断当前主机的字节序?
int checkCPU() { union w{ int a; char b; }c; c.a = 1; return (c.b == 1); }主机字节序到网络字节序
u_long htonl (u_long hostlong); u_short htons (u_short short); //掌握这个
网络字节序到主机字节序
u_long ntohl (u_long hostlong); u_short ntohs (u_short short);
9.6 IP地址转换
typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; }; in_addr_t inet_addr(const char *cp); //从人看的ip地址转为机器使用的32位无符号整数 char *inet_ntoa(struct in_addr in); //从机器到人示例
int main() { in_addr_t addr; addr = inet_addr("192.168.1.222"); printf("addr=0x%x\n", addr); struct in_addr tmp; tmp.s_addr = addr; printf("ip=%s\n", inet_ntoa(tmp)); }10. 复习
历史:
阿帕网:不能互联不同的主机、不同操作系统,没有纠错功能
TCP/IP:
IP
TCP
IP : A:首位固定为0。1byte网络号,3byte主机号
0000 0000 - 0111 1111 >0-127
123.0000...000 - 1111...111
255.0.0.0
B:首位固定为10。2byte网络号,2byte主机号
128.0 - 191.255.
172.125
255.255.0.0
C:首位固定为110。3byte网络号,1byte主机号
192.0.0-223.255.255
192.168.1
0-255 =》 254
255.255.255.0
子网掩码:网络全为1,主机号全为0.
22位网络号 10位主机号:
10 00...00 10 11...11
子网掩码:255.255.111111 00 .0000 0000
255.255.252.0
ip=网络号+主机号
网络号:是否处于同一网段
主机号: 唯一分配给主机的id
D(组播)E
port:端口。标识进程 udp 和 TCP端口独立
1-1023
>1023
socket - TCP/TP
IO-C b
网络设备 — socket - > fd
TCP流程:
服务器:
1.创建流式套接字socket .返回连接文件描述符
2.绑定(填充通信结构体)bind
3.监听。主动套接字变为被动套接字listen
4.阻塞等待客户端连接accept .返回通信文件描述符
5.收发消息
6.关闭套接字
客户端:
1.创建流式套接字socket
2.填充服务器的通信结构体
3.请求连接connect
4.收发消息
5.关闭套接字
【1】
基础理论:ip port socket 套接字类型 OSI TCP/IP udp TCP
核心编程框架:TCP UDP
UDP可以直接实现并发服务器
TCP-循环服务器
*TCP实现并发服务器。
引入linux IO模型4种:
1.阻塞IO:
特点-最常用、不能处理多路IO,效率低,不需要轮询,不浪费cpu资源
2.非阻塞:特点-不常用、能处理多路IO,需要轮询,耗费CPU
3.信号驱动IO:异步IO,需要底层驱动支持
4.IO多路复用 - 能实现TCP并发
select poll epoll
【2】UPD
服务器:
创建套接字(数据报套接字)
填充服务器的通信结构体
绑定
发收:
sendto
客户端:
创建套接字socket
填充服务器的通信结构体
发送
sendto(sockfd,buf,size,0,(struct sockaddr*)&saddr,
sizeof(saddr));
len=sizeof(caddr);
recvfrom(sockfd,buf,size,0,(struct sockaddr*)&caddr,&len);
非阻塞:
函数自带参数设置
fcntl(fd,功能选择,属性值(int))
F_GETFL F_SETFL F_SETOWN
IO多路复用:select
编程流程:
1.创建表
fd_set readfds,tempfds;
FD_ZERO(&readfds);
2.添加关心文件描述符到表中
FD_SET(0,&readfds);
// FD_SET(sockfd,&reafds);
...
3.调用函数检测
tempfds=readfds;
select(maxfd+1,&tempfds,NULL,NULL,NULL);
4.一个或多个文件描述符有事件产生返回
5.判断是那个文件描述符产生事件
if(FD_ISSET(0,&tempfds))
6.处理事件
{
fgets(buf,sizeof(buf),stdin);
}
if(FD_ISSET(sockfd,&tempfds))
{
acceptfd=accept();
}
select(检测文件描述符个数,读、写、异常,超时检测)
FD_SET:添加文件描述符到表中
FD_ZERO:清空表
FD_ISSET:判断对应文件描述符是否在表中
FD_CLR:从表中清除指定文件描述符
poll(表-结构体数组,数组有效元素的个数-检测文件描述符个数,-1->阻塞);
结构体:
fd
events:检测事件-POLLIN读 POLLOUT
revents:函数poll返回自动填充
如果对应fd有对应事件产生,将revents=events
如果对应fd没有对应事件产生,revents=0;
epoll:
int epfd=epoll_create(>0)
epoll_ctl(epfd,功能选择,fd,event-事件结构体)
功能选择:EPOLL_CTL_ADD 添加
EPOLL_CTL_MOD 修改已经添加事件
EPOLL_CTL_DEL 删除
event结构体:
data.fd
events: EPOLLIN|EPOLLET读
EPOLLOUT|EPOLLET 写
epoll_wait(epfd,事件存放的位置-事件结构体,数组元素个数,-1->阻塞);
多进程和多线程实现并发服务器思想:
每有一个客户端连接,创建一个子进程或线程和这个
客户端通信,父进程或主线程阻塞等待下一个客户端
连接。
fork创建进程的特点:
1.fork创建的子进程几乎拷贝了父进程所有的内容
三个段:正文、堆栈、数据段
2.fork之后父进程中返回子进程的PID,子进程中
返回0.
3.父进程先退出子进程孤儿进程,子进程先退出,
父进程没有回收资源,子进程僵尸进程。
4.fork之前的代码被复制,不会重新执行,fork之后
的代码会被复制并执行。
5.fork之前打开的文件,fork之后拿到的是同一个文件
描述符,操作同一个文件指针。
6.fork创建进程之后,两个进程就相互独立。
7.子进程状态发生改变会给父进程发送一个SIGCHLD信号
二、TCP编程
1.流程
服务器: socket:创建一个用与链接的套接字 bind:绑定自己的ip地址和端口 listen:监听,将主动套接字转为被动套接字 accept:阻塞等待客户端链接,链接成功返回一个用于通信套接字 recv:接收消息 send:发送消息 close:关闭文件描述符 客户端: socket:创建一个套接字 填充结构体:填充服务器的ip和端口 connect:阻塞等待链接服务器 recv/send:接收/发送消息 close:关闭
服务器: 1.创建流式套接字(socket())------------------------> 有手机 2.指定本地的网络信息(struct sockaddr_in)----------> 有号码 3.绑定套接字(bind())------------------------------>绑定手机 4.监听套接字(listen())---------------------------->待机 5.链接客户端的请求(accept())---------------------->接电话 6.接收/发送数据(recv()/send())-------------------->通话 7.关闭套接字(close())----------------------------->挂机 客户端: 1.创建流式套接字(socket())----------------------->有手机 2.指定服务器的网络信息(struct sockaddr_in)------->有对方号码 3.请求链接服务器(connect())---------------------->打电话 4.发送/接收数据(send()/recv())------------------->通话 5.关闭套接字(close())--------------------------- >挂机 测试注意: 如果使用客户端软件进行连接,必须保证windows和虚拟机在同一个局域网(桥接),并能互相ping通。服务器的IP地址必须指定为虚拟机自己的IP。 必须保证客户端正常退出后在关闭服务器程序,在客户端连接状态情况下强制关闭服务器程序,下次启动服务器程序后会提示bind err。这是因为没有正常释放绑定的端口,等1~2分钟就可以了。
2.函数接口
1.socket
int socket(int domain, int type, int protocol); 功能:创建套接字 参数: domain:协议族 AF_UNIX, AF_LOCAL 本地通信 AF_INET ipv4 AF_INET6 ipv6 type:套接字类型 SOCK_STREAM:流式套接字 SOCK_DGRAM:数据报套接字 protocol:协议 - 填0 自动匹配底层 ,根据type 系统默认自动帮助匹配对应协议 传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP 网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL) 返回值: 成功 文件描述符 失败 -1,更新errno
2.bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:绑定 ipv4 ip和端口 参数 sockfd:文件描述符 addr:通用结构体,根据socket第一个参数选择的通信方式最终确定这需要真正填充传递的结构体是那个类型。强转后传参数。 addrlen:填充的结构体的大小 返回值:0 失败-1、更新errno 通用结构体:相当于预留一个空间 struct sockaddr { sa_family_t sa_family; char sa_data[14]; } ipv4的结构体 struct sockaddr_in { sa_family_t sin_family; //协议族AF_INET in_port_t sin_port; //端口 struct in_addr sin_addr; }; struct in_addr { uint32_t s_addr; //IP地址 }; 本地址通信结构体: struct sockaddr_un { sa_family_t sun_family; //AF_UNIX char sun_path[108]; //在本地创建的套接字文件的路径及名字 }; ipv6通信结构体: struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; }; struct in6_addr { unsigned char s6_addr[16]; };3.listen
int listen(int sockfd, int backlog); 功能:监听,将主动套接字变为被动套接字 参数: sockfd:套接字 backlog:同时响应客户端请求链接的最大个数,不能写0. 不同平台可同时链接的数不同,一般写6-8个 (队列1:保存正在连接) (队列2,连接上的客户端) 返回值:成功 0 失败-1,更新errno
4.accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); accept(sockfd,NULL,NULL); 阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接, 则accept()函数返回,返回一个用于通信的套接字文件; 参数: Sockfd :套接字 addr: 链接客户端的ip和端口号 如果不需要关心具体是哪一个客户端,那么可以填NULL; addrlen:结构体的大小 如果不需要关心具体是哪一个客户端,那么可以填NULL; 返回值: 成功:文件描述符; //用于通信 失败:-1,更新errno
5.recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 功能: 接收数据 参数: sockfd: acceptfd ; buf 存放位置 len 大小 flags 一般填0,相当于read()函数 MSG_DONTWAIT 非阻塞 返回值: 0 成功接收的字节个数
6.connect
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 功能:用于连接服务器; 参数: sockfd:socket函数的返回值 addr:填充的结构体是服务器端的; addrlen:结构体的大小 返回值 -1 失败,更新errno 正确 07.send
ssize_t send(int sockfd, const void *buf, size_t len, int flags); 功能:发送数据 参数: sockfd:socket函数的返回值 buf:发送内容存放的地址 len:发送内存的长度 flags:如果填0,相当于write();
3.代码实现
优化代码
1.去掉fgets获取的多余的'\n'. if(buf[strlen(buf)-1] == '\n')//去掉fgets获取的'\n' buf[strlen(buf)-1] ='\0'; 2.端口和ip地址通过命令行传参到代码中。 3.设置客户端退出,服务器结束循环接收。 通过recv返回值为0判断客户端是否退出 4.设置来电显示功能,获取到请求链接服务器的客户端的ip和端口。 5.设置服务器端自动获取自己的ip地址。 INADDR_ANY "0.0.0.0" 6.实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。
server.c代码
#include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } // 1.创建流式套接字socket .返回连接文件描述符 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfdclient.c代码
#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if(argc != 3) { printf("please input %s \n",argv[0]); return -1; } // 1.创建流式套接字socket .返回连接文件描述符 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd4.tcp实现ftp功能
模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。
项目功能介绍: 均有服务器和客户端代码,基于TCP写的。 在同一路径下,将客户端可执行代码复制到其他的路径下,接下来再不同的路径下运行服务器和客户端。 相当于另外一台电脑在访问服务器。 客户端和服务器链接成功后出现以下提示:四个功能 ***************list************** //列出服务器所在目录下的文件名(除目录不显示) ***********put filename********** //上传一个文件 ***********get filename********** //重服务器所在路径下载文件 **************quit*************** //退出(可只退出客户端,服务器等待下一个客户端链接)
server.c代码
#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include void put_server(int acceptfd, char *buf, int size); void list_server(int acceptfd, char *buf, int size); int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } // 1.创建流式套接字socket .返回连接文件描述符 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd d_name[0] == '.') continue; //opendir(".") //获取文件属性,判断是普通文件发送给客户端 stat(file->d_name, &st); if (S_ISREG(st.st_mode)) { send(acceptfd, file->d_name, size, 0); } } //发送结束标志 strcpy(buf, "send ok"); send(acceptfd, buf, size, 0); } //put:新建打开文件,接收写文件 void put_server(int acceptfd, char *buf, int size) { int fd = open(buf + 4, O_WRONLY | O_CREAT | O_TRUNC, 0666) ; if (fdclient.c代码
#include #include #include #include #include #include #include #include #include #include #include #include #include void show(void); void put_client(int sockfd,char *buf,int size); void list_client(int sockfd,char *buf,int size); int main(int argc, char const *argv[]) { if(argc != 3) { printf("please input %s \n",argv[0]); return -1; } // 1.创建流式套接字socket .返回连接文件描述符 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd 绑定号码(发短信知道发给谁) 接收信息(recvfrom())--------------------->接收短信 关闭套接字(close())----------------------->接收完毕 client: 创建数据报套接字(socket())----------------------->有手机 指定服务器的网络信息------------------------------>有对方号码 发送信息(sendto())---------------------------->发送短信 关闭套接字(close())--------------------------->发送完2.函数接口
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t*addrlen); 功能:接收数据 参数: sockfd:套接字描述符 buf:接收缓存区的首地址 len:接收缓存区的大小 flags:0 src_addr:发送端的网络信息结构体的指针 addrlen:发送端的网络信息结构体的大小的指针 返回值: 成功接收的字节个数 失败:-1 0:客户端退出 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 功能:发送数据 参数: sockfd:套接字描述符 buf:发送缓存区的首地址 len:发送缓存区的大小 flags:0 src_addr:接收端的网络信息结构体的指针 addrlen:接收端的网络信息结构体的大小 返回值: 成功发送的字节个数 失败:-13.实现
server.c代码
#include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { //1.创建数据报套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfdclient.c代码
#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { //1.创建数据报套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd4. 练习:实现如客户端发送"hello"给服务器端,服务器接着给客户端回,"recv:hello!!!!!"。
注意: 1、对于TCP是先运行服务器,客户端才能运行。 2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系, 3、一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。 4、UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。 5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。
#include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } // 1.创建流式套接字socket .返回连接文件描述符 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd5.项目-网络聊天室
5.1 项目要求
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
问题思考
- 客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
- 有几种消息类型?
- 登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
- 聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
- 退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
- 服务器如何存储客户端的地址?
数据结构可以选择线性数据结构
链表节点结构体: struct node{ struct sockaddr_in addr;//data memcmp struct node *next; }; 消息对应的结构体(同一个协议) typedef struct msg_t { int type;//'L' C Q enum un{login,chat,quit}; char name[32];//用户名 char text[128];//消息正文 }MSG_t; int memcmp(void *s1,void *s2,int size)- 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。
5.2 程序流程图
服务器端
客户端
客户端
server.c代码
#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct sockaddr_in serveraddr,caddr; enum type_t//枚举 { Login, Chat, Quit, }; typedef struct MSG { char type;//L C Q char name[32];// char text[128];// }msg_t; typedef struct NODE//链表 { struct sockaddr_in caddr; struct NODE *next; }node_t; node_t *create_node(void)//建头节点 { node_t *p=(node_t *)malloc(sizeof(node_t)); if(p==NULL) { perror("malloc err"); return NULL; } p->next=NULL; return p; } void do_login(int ,msg_t ,node_t *,struct sockaddr_in);//登录的函数 void do_chat(int ,msg_t ,node_t *,struct sockaddr_in);//群聊的函数 void do_quit(int ,msg_t ,node_t *,struct sockaddr_in);//退出函数 int main(int argc, char const *argv[]) { if(argc !=3) { printf("Usage:./a.out \n"); return -1; } //创建UDP套接字 int sockfd = socket(AF_INET,SOCK_DGRAM,0); if(sockfdnext; sendto(sockfd,&msg,sizeof(msg),0,(struct sockaddr *)&(p->caddr),sizeof(p->caddr)); //printf("%s\n",msg.text); } node_t *new=(node_t *)malloc(sizeof(node_t)); //初始化 new->caddr=caddr; new->next=NULL; //链接到链表尾 p->next=new; return; } //群聊的函数 //功能:将客户端发来的聊天内容转发给所有已登录的用户,除了发送聊天内容的用户以外 void do_chat(int sockfd,msg_t msg,node_t *p,struct sockaddr_in caddr) { //遍历链表 while(p->next != NULL) { p=p->next; if(memcmp(&(p->caddr),&caddr,sizeof(caddr)) != 0) { sendto(sockfd,&msg,sizeof(msg),0,(struct sockaddr *)&(p->caddr),sizeof(p->caddr)); } } return; } //退出函数 //功能: //1》将谁退出的消息转发给i所有用户 //2》将链表中保存这个推出的用户信息的节点删除 void do_quit(int sockfd,msg_t msg,node_t *p,struct sockaddr_in caddr) { sprintf(msg.text,"%s 以下线",msg.name); while(p->next != NULL) { if((memcmp(&(p->next->caddr),&caddr,sizeof(caddr)))==0) { node_t *dele=NULL; dele = p->next; p->next=dele->next; free(dele); dele=NULL; } else { p=p->next; sendto(sockfd,&msg,sizeof(msg),0,(struct sockaddr *)&(p->caddr),sizeof(p->caddr)); } } return; }client.c代码
#include #include #include #include #include #include #include #include #include #include #include #include #include #include enum type_t { Login, Chat, Quit, }; typedef struct { char type;//L C Q char name[32];// char text[128];// }msg_t; int main(int argc, char const *argv[]) { if(argc !=3) { printf("Usage ./a.out \n"); return -1; } int sockfd = socket(AF_INET,SOCK_DGRAM,0); if(sockfdp int fd; /* 检测的文件描述符 *//p p short events; /* 检测事件 *//p p short revents; /* 调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员。只需要判断这个成员的值就可以确定是否产生事件 *//p p };/p p 事件: POLLIN :读事件/p p POLLOUT : 写事件/p p POLLERR:异常事件/p p /p h4poll实现IO多路复用的特点/h4 p /p pre class="brush:python;toolbar:false"1. 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定) 2. poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低 3. poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可/pre h4实现代码/h4 pre class="brush:python;toolbar:false"#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } // 1.创建流式套接字socket .返回连接文件描述符 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd 读事件 //1.创建表 struct pollfd fds[200] = {}; //2.将关心文件描述符添加到表中 fds[0].fd = 0; fds[0].events = POLLIN; fds[1].fd = sockfd; fds[1].events = POLLIN; int last = 1; char buf[64]; while (1) { //调用poll监测 int ret = poll(fds, last + 1,2000); //阻塞 if (ret3.3 利用setsockopt属性设置
Linux中socket属性
选项名称 说明 数据类型 ======================================================================== SOL_SOCKET 应用层 ------------------------------------------------------------------------ SO_BROADCAST 允许发送广播数据 int SO_DEBUG 允许调试 int SO_DONTROUTE 不查找路由 int SO_ERROR 获得套接字错误 int SO_KEEPALIVE 保持连接 int SO_LINGER 延迟关闭连接 struct linger SO_OOBINLINE 带外数据放入正常数据流 int SO_RCVBUF 接收缓冲区大小 int SO_SNDBUF 发送缓冲区大小 int SO_RCVLOWAT 接收缓冲区下限 int SO_SNDLOWAT 发送缓冲区下限 int SO_RCVTIMEO 接收超时 struct timeval SO_SNDTIMEO 发送超时 struct timeval SO_REUSEADDR 允许重用本地地址和端口 int SO_TYPE 获得套接字类型 int SO_BSDCOMPAT 与BSD系统兼容 int ========================================================================== IPPROTO_IP IP层/网络层 ---------------------------------------------------------------------------- IP_HDRINCL 在数据包中包含IP首部 int IP_OPTINOS IP首部选项 int IP_TOS 服务类型 IP_TTL 生存时间 int IP_ADD_MEMBERSHIP 将指定的IP加入多播组 struct ip_mreq ========================================================================== IPPRO_TCP 传输层 ----------------------------------------------------------------------------- TCP_MAXSEG TCP最大数据段的大小 int TCP_NODELAY 不使用Nagle算法 int
API接口
int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen) int setsockopt(int sockfd,int level,int optname,void *optval,socklen_t optlen) 功能:获得/设置套接字属性 参数: sockfd:套接字描述符 level:协议层 SOL_SOCKET(应用层) IPPROTO_TCP(传输层) IPPROTO_IP(网络层) optname:选项名 SO_BROADCAST 允许发送广播数据 int SO_RCVBUF 接收缓冲区大小 int SO_SNDBUF 发送缓冲区大小 int SO_RCVTIMEO 接收超时 struct timeval SO_SNDTIMEO 发送超时 struct timeval optval:选项值 optlen:选项值大小指针
#include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { if (argc != 2) { printf("please input %s \n", argv[0]); return -1; } // 1.创建流式套接字socket .返回连接文件描述符 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd设置超时检测操作
struct timeval { long tv_sec; /*秒*/ long tv_usec; /*微秒*/ }; //设置接收超时 struct timeval tm={2,0}; setsockopt(acceptfd,SOL_SOCKET,SO_RCVTIMEO,&tm,sizeof(tm)); //设置超时之后时间到打断接下来的阻塞在这个文件描述符的函数,直接错误返回 补充: //设置端口和地址重用 int optval=1; setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval));3.4 alarm定时器设置
alarm(5) 闹钟 定时器 //5秒之后会,会有一个信号产生(SIGALRM) int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 功能:对接收到的指定信号处理 signum 信号 struct sigaction { void (*sa_handler)(int); }; //设置信号属性 struct sigaction act; sigaction(SIGALRM,NULL,&act);//获取原属性 act.sa_handler=handler;//修改属性 sigaction(SIGALRM,&act,NULL);//将修改的属性设置回去 注:在recv前调用alarm函数 alarm的 SIGALRM信号产生后会打断(终端)下面的系统调用recv; 打断后相当于recv错误返回。#include #include void handler(int sig) { printf("time out -----------------\n"); } int main(int argc, char const *argv[]) { //SIGALRM struct sigaction act; //1.获取原属性 sigaction(SIGALRM,NULL,&act); //2.修改 act.sa_handler=handler; //3.设置 sigaction(SIGALRM,&act,NULL); char buf[32]; while(1) { alarm(2); if(fgets(buf,sizeof(buf),stdin)== NULL) { perror("fgets err."); } printf("buf:%s\n",buf); } return 0; }六、广播、组播、本地套接字通信
1. 广播
1.1 理论
- 前面介绍的数据包发送方式只有一个接受方,称为单播
- 如果同时发给局域网中的所有主机,称为广播
- 只有用户数据报(使用UDP协议)套接字才能广播
- 一般被设计成局域网搜索协议
- 广播地址
- 以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址
- 发到该地址的数据包被所有的主机接收
1.2 广播发送流程
- 创建用户数据报套接字
- 缺省创建的套接字不允许广播数据包,需要设置属性(setsockopt)
- 接收方地址指定为广播地址
- 指定端口信息
- 发送数据包
#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { //1.创建数据报套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd1.3 广播接收 流程
- 创建用户数据报套接字
- 绑定IP地址(广播IP或0.0.0.0)和端口
- 绑定的端口必须和发送方指定的端口相同
- 等待接收数据
#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { //1.创建数据报套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd2. 组播
2.1 理论
- 单播方式只能发给一个接收方。
- 广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。
- 组播是一个人发送,加入到多播组的人接收数据。
- 多播方式既可以发给多个主机,又能避免象广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
2.2 组播地址
不分网络地址和主机地址,第1字节的前4位固定为1110 。是D类IP
224.0.0.1 – 239.255.255.255
2.3 组播发送
- 创建用户数据报套接字
- 接收方地址指定为组播地址
- 指定端口信息
- 发送数据包
#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd
2.4 组播接收
- 创建用户数据报套接字
- 加入多播组
- 绑定IP地址(加入组的组IP或0.0.0.0)和端口
- 等待接收数据
#include #include #include #include #include #include #include #include #include #include int main(int argc, char const *argv[]) { int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd sqlite3数据库开发支持库 # apt-get install sqlite3-doc --->sqlite3数据库说明文档 -------------------------------- #apt-get install sqlitebrowser --->sqlite3数据库操作软件源码安装:
tar xf sqlite-autoconf-3140100.tar.gz ./configure make sudo make install
安装完成后,可以使用sqlite3 -version命令来测试是否安装成功
$ sqlite3 -version 3.14.1 2016-08-11
3. 基础SQL语句使用
【腾讯文档】sqlite基础SQL语句使用
sqlite基础SQL语句使用
4. sqlite使用入门
数据库 · 华清远见教学空间
5. sqlite3编程
API接口文档
官方文档:List Of SQLite Functions
中文文档:SQLite 命令 - SQLite 中文版 - UDN开源文档
头文件:#include 编译:gcc sqlite1.c -lsqlite3 1.int sqlite3_open(char *path, sqlite3 **db); 功能:打开sqlite数据库,如果数据库不存在则创建它 path: 数据库文件路径 db: 指向sqlite句柄的指针 返回值:成功返回SQLITE_OK,失败返回错误码(非零值) 2.int sqlite3_close(sqlite3 *db); 功能:关闭sqlite数据库 返回值:成功返回SQLITE_OK,失败返回错误码 返回值:返回错误信息 3.执行sql语句接口 int sqlite3_exec( sqlite3 *db, /* An open database */ const char *sql, /* SQL to be evaluated */ int (*callback)(void*,int,char**,char**), /* Callback function */ void *arg, /* 1st argument to callback */ char **errmsg /* Error msg written here */ ); 功能:执行SQL操作 db:数据库句柄 sql:要执行SQL语句 callback:回调函数(满足一次条件,调用一次函数,用于查询) 再调用查询sql语句的时候使用回调函数打印查询到的数据 arg:传递给回调函数的参数 errmsg:错误信息指针的地址 返回值:成功返回SQLITE_OK,失败返回错误码 回调函数: typedef int (*sqlite3_callback)(void *para, int f_num, char **f_value, char **f_name); 功能:select:每找到一条记录自动执行一次回调函数 para:传递给回调函数的参数(由 sqlite3_exec() 的第四个参数传递而来) f_num:记录中包含的字段数目 f_value:包含每个字段值的指针数组(列值) f_name:包含每个字段名称的指针数组(列名) 返回值:成功返回SQLITE_OK,失败返回-1,每次回调必须返回0后才能继续下次回调 4.不使用回调函数执行SQL语句(只用于查询) int sqlite3_get_table(sqlite3 *db, const char *sql, char ***resultp, int *nrow, int *ncolumn, char **errmsg); 功能:执行SQL操作 db:数据库句柄 sql:SQL语句 resultp:用来指向sql执行结果的指针 nrow:满足条件的记录的数目(但是不包含字段名(表头 id name score)) ncolumn:每条记录包含的字段数目 errmsg:错误信息指针的地址 返回值:成功返回SQLITE_OK,失败返回错误码 5.返回sqlite3定义的错误信息 char *sqlite3_errmsg(sqlite3 *db);#include #include int callback(void *arg, int f_num, char **f_value, char **f_name) { printf("%s\n", (char *)arg); for (int i = 0; icallback函数的使用
#include //(a+b) * c //(a-b) * c //(a*b) * c //(a/b) * c //a+b a-b a*b a/b int add(int a,int b) { return a+b; } int sub(int a,int b) { return a-b; } int mul(int a,int b) { return a*b; } int chu(int a,int b) { return a/b; } //int (* fun_p)(int,int); //fun_p=add; fun_p = mul; int fun(int c,int (*fun_p)(int ,int ), int a,int b) { return fun_p(a,b)*c; } //想通过一个函数更改一个变量的值: //两种- 参数(传变量地址),返回值 #if 0 int func(void) { return 1000; } void func1(int *sp) { *sp=1000; } #endif int *func(void) { static int a=1000; return &a; } void func1(int **sp) { static int a=1000; *sp=&a; } int main(int argc, const char *argv[]) { printf("%d\n",fun(10,add,2,3)); printf("%d\n",fun(10,sub,2,3)); printf("%d\n",fun(10,mul,2,3)); printf("%d\n",fun(10,chu,2,3)); // int a;//1000; // a=fun();//fun1(&a); int *p; int **q; return 0; }
- 客户端如何同时处理发送和接收?
- 客户端会不会知道其它客户端地址?














