#网络编程
自我介绍 { 杨航 ---> 航哥 } 课堂 { 1.不能迟到 2.不懂要问 3.作业检查 } linux网络编程 { 优先高! 进程间通信 { 管道: 1.有名管道 2.无名管道 3.信号 IPC通信 { 4.共享内存 5.消息队列 6.信号灯 } 7.套接字socket }
网络历史
{
1968- ARPAnet(阿帕网) 是Internet"雏形". 不能跨平台 操作系统不兼容
1974- 第一份TCP详细协议说明. 不能进行错误检测.(没有纠错功能)
1983- TCP/IP 协议 成为了"世界语"
1.对数据具备纠错功能(TCP)
2.能够准确找到目标主机(IP)
}
协议
{
一种规则(两者之间的"约定");
}
OSI网络协议层(有7层协议, 理想协议层)
{
1.应用层:
2.表示层: 加密
3.会话层: 建立新的连接节点
4.传输层:
5.网络层:
6.数据链路层:
7.物理层:
}
TCP/IP协议层(4层, 标准协议层)
{
重点!!
应用层:http(超文本传输协议),ftp(文件传输协议),nfs(挂载协议),ssh(远程登录协议)
传输层:TCP(传输控制协议), UDP(用户数据报协议)
网络层:IP(网间协议) , ICMP, IGMP
物理接口层(数据链路与物理): 以太网协议, ARP(地址解析协议)
套接字编程重点在应用层:
其他协议封装由内核完成.!!
}
网络编程接口
{
套接字---> 非负整数,类似于文件描述符,是网络通信的接口.
socket() ----> 创建套接字.
int socket(int domain, int type, int protocol);
{
domain: 选择网络层IP协议类型(AF_UNIX(本地), AF_INET(ipv4),
AF_INET6(ipv6))
type:
决定套接字的类型:
流式套套接字:SOCK_STREAM 数据的传输以字节流的方式
在传输层TCP协议选择的就是该方式
数据报套接字:SOCK_DGRAM 数据是以报文方式传输.
在传输层UDP协议选择的就是该方式
protocol: 默认传递 0
该参数是与第二个参数有关系的
第二个参数:SOCK_STREAM 第三个为0 则表示选择TCP协议
第二个参数:SOCK_DGRAM 第三个为0 则表示选择UDP协议
返回值:
成功:返回监听套接字
错误: 返回 -1
}
TCP服务器的创建流程
{
1.买手机 ----> 创建套接字 socket()
2.买卡,与手机进行绑定 ----> 绑定函数 bind()
{
绑定ip地址: 标识主机(找到目标pc机)
绑定port端口: 标识进程(找到pc上对应的进程)
}
3.等电话 ------> 监听 listen()
4.接听电话 ----> 接受客户端连接请求 accept()
{
如果accept成功,返回一个通信套接字(连接套接字)
该套接字用于后续与对应客户端的数据交互.
}
数据的接收与发送!!
{
read()/write();
recv()/send();
recv(connfd, buf, sizeof(buf), 0);
0:表示阻塞方式(默认方式)
}
5.挂电话 ----> 关闭套接字 close()
端口(ushort):
取值范围: 0 ~ 65535 0~1023系统使用
应用程序: 1024 ~ 65535
字节序(与处理器有关系)
{
小端序(inter芯片X86架构处理器): 低地址存放低字节
大端序(ARM处理器、网络传递数据方式): 低地址存放高字节
字节转换函数: htons() : 主机转网络字节序
ntohs() : 网络转主机字节序
}
}
}
}
day2:数据结构有什么用:是拿来做操作系统和服务器的一个核心 tcp服务器 { 同时监听和同时处理是两个概念。 soket():创建套接字 bind():port,ip绑定地址与端口信息, listen(): 当accept成功返回说明三次握手成功。 accept() { read()的返回值 { 1,对文件操作 >0:表示正确读取到的字节数 ==0:表示读到了文件末尾 ==-1: 2,对管道操作的返回值(管道是半双工的(在同一时间只能读或写)) >0:读取到的实际字节数 ==0:写端已经关闭 ==-1:表示错误 3.对套接字操作的返回值(套接字是全双工的) >0:读取到的实际字节数 ==0:当对方关闭套接字时(检测客户端或者服务器是否断开) ==-1:表示错误 }
自动缩进:命令行v进入可视,然后(方向键)选择,按=号缩进。
32位系统,地址占4个字节
64位系统,地址占8个字节
小端序反着取,大端序正着取
atoi():字符串转整型数据
htons:字节序转换,小端换大端
客户端只有一个套接字
memset():清空缓冲区操作
TCP客户端流程
{
1.socket()
2.设置服务器地址与端口 struct sockaddr_in (ipv4).
3.connect() 请求连接
数据交互!
4.close()
}
三次握手过程
{
netstat:查看网络状态指令。(一般和grep -r配合)
//netstat -apn | grep "8888"
三次握手:只在tcp协议才会有该过程,UDP没有
TCP中要保证点对点链接,数据传输无误。
服务器中在accept函数完成,在客户端有connext函数完成
客户端->服务器:
1.客户端connect函数发送SYN(待确认数据包标志) J包给服务器(发送完客户端会进入SYN_SEND状态)等待状态)
2.服务器收到客户端SYN,服务器accept函数回传ACK( ACK=J+1)( 已确认标志,确认收到SYN)和SYN(待确认标志)给客户端(服务器进入了等待状态)
发送 SYN N包,进入SYN_RECV状态
3.客户端收到服务器SYN N包,客户端回传ACK N+1给服务器,发送之后双方进入(ESTABLISHED状态)通信状态,三次握手成功
TCP协议特点
{
1.建立链接(点对点的连接:三次握手来保证)
2.能够保证数据可靠,不丢失,不失序:TCP协议里面数据包里的序号(之前传输的字节数)和确认号(本次传输的字节数)
}
}
四次挥手:
{
主动方第一次关闭的是套接字里面的读写缓存区(不能收发消息,但是还能收发标志),第二次完全断开连接。每一次关闭都有一次对方的确认消息。
1.主动断开连接(close()),向被动端发送FIN M包,进入FIN_WAIT1状态。
2.被动端收到关闭请求,发送确认包ACK M+1.主动端进入FIN_WAIT2,被端进入CLOSE_WAIT状态
3被动端发送关闭请求 FIN N 包,进入LAST_FIN状态。主动端进入TIME_WAIT状态
4.主动端给被动端 ACK N+1 确认包,被动端进入CLOSE状态,主动端TIME_WAIT状态持续一定时间(与系统有关Linux系统一般40s),最后进入CLOSE
}
通过简单的socket可以实现一对一的c/s通信,当多个客户端同时进行服务器访问,那么服务器只能按序的一一进行处理,
除了第一个客户端,其余客户端都会陷入等待。并且这样的程序只能实现半双工通信(数据能双向传输,但同一时刻只能单向传递,通过切换传输方向实现双工),
而且实现方式繁琐,功能拘束,实用价值很低。那么要想实现一个服务器能同时接受多个客户端访问并且能够双工通信的并发服务器,其中一种实现方式----多进程。
并发服务器
{
线程并发:难点:参数问题,同步与互斥
进程并发:难点:资源回收
}
*多线程 与多进程服务器模型(统称为并发服务器)
{
进程并发
{
1.socket();
2.bind();
3.listen();
signal(SIGCHLD,signalHandle);
while(1)
{
4.accept();
//accept成功返回后就fork//
if((pid=fork())<0)
{
perror("fork");
exit(-1);
}
else if(pid==0)
{
close(socketfd);
/*与客户端数据交互*/
while(1)
}
close(connfd);
}
5.close();
}
}
线程并发:
{ /要求:需要打印信息是来自哪个客户端*/
1.socket();
2.bind();
3.listen();
while(1)
{
4.accept();
pthread_create();//传递结构体(传递客户端信息以及传通信套接字(封装到结构体里))
pthread_detach();//设置分离
}
5.close();
线程的属性:
结合:子线程的资源需要主线程来回收(线程之间可能会相互影响)
分离:把子线程完全分离(独立),当子线程退出后自动回收资源。
设置分离属性:pthread_detach()
} 练习:并发服务器的创建! }
作业: 1.消化今天的内容
2.实现线程并发服务器,并且接收信息时, 输出来自于哪个客户端.
====================================================================
day3:复习 { 三次握手 { 1.客户端给服务器发送SYN J包(待确认包),自身进入SYN_SEND状态 2.服务器收到客户端SYN J,回传ACK J+1确认标志(确认收到SYN), 发送SYN N包 进入SYN_RECV状态 3.客户端收到服务器SYN N包, 回传一个ACK N+1表示确认,双方都进入 ESTABLISHED状态,三次握手成功. } 四次挥手 { 主动断开连接与被动断开连接.
1.主动端主动断开连接(close()),向被动端发送FIN M包.进入FIN_WAIT1状态
2.被动端收到关闭请求,发送确认包ACK M+1.
主动端进入FIN_WAIT2,被动端进入CLOSE_WAIT状态
3.被动端发送关闭请求 FIN N 包, 进入LAST_FIN状态.主动端进入TIME_WAIT状态
4.主动端给被动端发送 ACK N+1 确认包, 被动端进入CLOSE状态,
主动端TIME_WAIT状态持续一定时间(与系统有关Linux系统一般40s),最后进入CLOSE
}
并发服务器
{
线程并发: 难点: 参数问题, 同步与互斥
进程并发: 难点: 资源回收
}
} UDP协议 { 可以用做广播或者组播(多播) 与TCP共同点:同为传输层协议,协议头中都用端口(标识进程)。 不同: 1.TCP面向连接,能保证数据可靠,不失序,传输速率较慢(有三次握手) 应用场景:用户名 密码登陆时,传输重要的大文件。
2.UDP无连接,不能够保证数据可靠,也不能保证顺序,传输速率较快(无三次握手)
应用场景:常见的流媒体软件,qq传输消息
}
UDP服务器 { 1.socket() //创建数据报套接字 SOCK_DGRAM 2.bind() // 绑定 目的:显示地址信息,让其他客户端第一次能够找到他。 3.sendto()数据发送//recvfrom() 数据接收(recvfrom只有管道破裂才会返回0) recvfrom参数: sockfd:套接字 buf:保存接收数据的首地址 len:接收的数据长度 falg:接收方式 一般为0代表默认 以阻塞模式 src_addr:保存对方的ip与port信息 addrlen:需要保存的地址信息长度
返回值:
>0:正确接收到的字节数
==0:对方关闭了套接字
==-1:错误
4.close()
} UDP客户端 { 1.socket() 数据报套接字 SOCK_DGRAM. 2.设置服务器的地址信息 struct sockaddr_in. 3.sendto()/recvfrom() 数据收发 4.close() } sprintf() { 格式化字符串函数. int sprintf(char *str, const char *format, ...); { str: 字符串缓存的首地址 format: 格式... ...: 表达式
例如: sprintf(buf, "%s %d", name, len);
}
int a = 14;
float f = 12.34;
char buf[32] = {0};
sprintf(buf ,"int:%d Float:%f", a,f);
}
} 作业: 1.消化今天的课程内容
2.UDP聊天室项目思路整理.
====================================================================
day4: 复习: udp与tcp 同为传输层协议 tcp:面向连接,保证数据可靠,不失序,传输效率较低 udp:无连接,不保证数据可靠,失序,传输效率较高
UDP: recvfrom:获取对方的地址信息(ip和port)
sendto:地址信息不能为空
connect函数是可以被调用的,但是没有三次握手过程,只是表示单纯连接到服务器。 连接后可直接使用recv与send。只能给对应连接的服务器发送消息。
服务器模型: { 循环服务器(tcp与udp) 并发服务器(tcp与udp)
多路IO服务器
{
}
阻塞IO:
{
read(),write(),fgets(),recv(),send()...等
读阻塞:
当读缓存区里面没有数据可读时,就会发生阻塞,内核会自动记录该操作
当缓存区有数据可以读取时,内核立马将应用进程唤醒,程序进行读取
写阻塞:
当写缓存区被写满时(不多见),发生阻塞。
当缓存区里有任何可写入空间时,内核就会将应用程序唤醒
缺点:阻塞会影响其他功能的运行,不能达到想要的程序效果。
}
非阻塞IO:
{
函数:sendto(),在udp协议中属于非阻塞函数(没有写缓存)
可以利用fcntl()函数来设置非阻塞;
int fcntl(int fd,int cmd,...)
F_GETFL:获取当前文件的模式
例:
int flag=0;
fcntl(0,F_GETFL, &flag);
flag=flag | O_NONBLOCK;
fcntl(0,F_SETFL,flag);
非阻塞模式的缺点:函数需要不停的轮巡检测是否可操作,比较消耗cpu的资源。
}
为了解决以上两种问题,引出:IO多路复用
IO多路复用:
特点:支持多个IO操作,并不会阻塞程序功能,
也不会像非阻塞一样一直轮巡检测。
select的原理:循环监听一张文件描述符表,当监听的文件描述符进行io操作时
select会将对应的程序唤醒
select()函数:(相当于内核的秘书,给内核记录,通知内核,让内核区操作,监听操作)
在内核里创建了一张文件描述符表,内核自动对表进行监听,如果发现有任何IO操作,则反馈给应用程序。没有,则阻塞。(不会受到其他文件描述符的影响)
利用位操作,只有0和1,1代表需要监听的文件描述符
select()操作:
1.首先创建一张文件描述符表
表的类型:fd_set(利用了位图的原理实现)
fd_set 是一个结构体,该结构体128字节-->1024位;
fd_set fds;//定义文件描述符表
2.对表进行初始化
清空,再加入文件描述符
void FD_ZERO(fd_set *fds);//将表清空,全置0
void FD_SET(int fd,fd_set *fds);//将表内哪个文件描述符(第几位文件描述符)置1;用于往表里添加文件描述符
3.初始化完成后就交给select函数来监听
int FD_ISSET(int fd,fd_set *fds) //检测fd对应的文件描述符是否可操作,返回1说明有io操作
void FD_CLR(int fd,fd_set *fds)//从表中删除对应的文件描述符
参数:int nfds:最大的位置的文件描述符+1的值(fd+1)
fd_set * readfds:需要监听的读文件描述符表
fd_set *writefds:写文件描述符表(没有的就传NULL)
fd_set* exceptfds:可能会出现异常的文件描述符表(NULL)
struct timeval *timeout:设置等待的时间(也可以设NULL,代表一直监听)
例如: struct timeval tm={5,0};//设置为5秒,后面的参数为微秒设置
或tm.tv_sec=8; tm.tv_usec=0;//设置为8秒
select返回值:
错误:-1;
成功:返回三张表总共能操作的文件描述符个数//一般都是1
==0:在没有任何io操作时并且定时时间到了
while(1)
{ temp_fds=rfds;//备份文件描述符表
tm.tv_sec=8; //重置等待时间
tm.tv_usec=0;
if((num=select(maxfd,&temp_fds,NULL,NULL,&tm))<=0)//时间会一直减
select的作用:
{1.会将操作的文件描述符保留(保持1),将其他监听的文件描述符置0,然后传给应用程序
2.select进行监听
}
if(num==0)
{
printf("timeout===");
continue;
}
for(i=0;i<maxfd;i++)
{
if(FD_ISSET( i , &temp_fds)==1)//判断i文件符是否可操作
{
if(i=0)
{ memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("buf=%s",buf);
}
else
{
memset(buf1,0,sizeof(buf1));
read(fd,buf1,sizeof(buf1));
}
}
}
}
select()函数:(缺点)
{
1.最多操作1024个文件描述符(1024就是上限)
2.初始化的表与返回表没有做分离
3.每次都需要循环检测表里面的文件描述符。
}
poll()函数:
{
1.没有1024的限制,可以通过修改配置文件来增加文件描述符的上限
2.实现了初始化表与返回表的分离
3.每次都需要循环检测表里面的文件描述符。(缺点)
}
修改配置文件:
ulimit -a:可以查看文件描述符上限(open files的值)
修改该配置文件:vim /etc/security/limits.conf(当硬件数大于软件才可以修改)
* soft nofile 2000
* hard nofile 15000
int poll(struct pollfd *fds,nfds_t nfds,int timeout )//函数原型
struct pollfd rfds[SIZE];
定义一个数组(数组的长度就是最多操作文件描述符的上限),
数组里面每一个元素都是系统的结构体(struct pollfd)
还需要定义一个表尾指针,记录最后一个元素的下标
int last=-1;
删除元素(但本质上并没有删除):将元素的的fd赋值为-1(系统检测到就会跳过这个元素),last不变
struct pollfd成员:
int fd;//需要检测的文件描述符
short events;//需要监听该文件描述符的什么操作(POLLIN:读操作 POLLOUT:写操作)
short revents;//poll返回的时会自动赋值(POLLIN,POLLOUT,POLLERR)
,它实现了分离初始化表和返回表的操作
int poll(struct pollfd *fds,nfds_t nfds,int timeout )//函数原型
参数:
fds:结构体数组首地址
nfds:需要监听的文件描述符的最大个数(last+1)
timeout:定时时间 单位us(微秒) -1:代表一直监听
如何判断可执行的文件描述符:if(rfds[i].revents&POLLIN)//&按位与(POLLIN=0x0001);
添加元素:找到fd=-1的位置,添加进去
/*===============================================================
-
Copyright (C) 2021 All rights reserved.
-
文件名称:poll.c(io多路复用poll函数)
-
创 建 者:杨航
-
创建日期:2021年08月09日
-
描 述: *
-
更新日志: * ================================================================*/ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <strings.h> #define SIZE 2000 int main(int argc, char *argv[]) { /存放文件描述符数组/ struct pollfd rfds[SIZE];//自己定义一个结构体数组大小,可通过配置文件修改上限 memset(rfds, 0 , sizeof(rfds));//将结构体数组清零 int i = 0; for(i = 0; i < SIZE; i++) { rfds[i].fd = -1;//初始化,将每一个文件描述符置为-1; } int lastPos = -1;//定义表尾指针并初始化 rfds[++lastPos].fd = 0; //将标准输入加入数组 rfds[lastPos].events = POLLIN; //定义events为读操作
rfds[++lastPos].fd = sockfd; //将监听套接字加入数组 rfds[lastPos].events = POLLIN; //定义为读操作
int i = 0; while(1) { if(-1 == poll(rfds, lastPos+1, -1)) { perror("poll"); exit(-1); } for(i = 0; i <= lastPos; i++) { if(rfds[i].revents & POLLIN)//判断当前文件描述符是否可执行 { if(i == 0) { //执行对应操作. } } } } return 0; }
==================================================================== day5:select():可以跨平台使用(windows, MAC os)
{
文件描述符表.
1.创建表 fd_set
2.往表当中加入要操作的文件描述符. FD_SET()
3.交给select进行轮询监听.
}
poll
{
1.能够突破1024限制,通过该配置文件来完成.
2.实现初始表与返回表的分离.
3.还是需要轮询检测表中的文件描述符(缺点).
}
平衡二叉树(保证全局平衡):为使查找效率变高而生 1.任意子树右孩子的值比根节点大,左孩子的值比根节点的值小 2.任意子树的层数差值必须小于等于1
红黑树(保证局部平衡):查找效率不错,且添加删除元素更加方便
epoll机制实现(为了解决循环查询的问题), 对树进行监听,当监听到可操作的文件描述符时, 会将可操作的文件描述符放入数组 返回值代表有多少个正在操作的文件描述符(很小,大大提高了效率) 实现了分离,操作的是树,返回操作的是数组表
只能在Linux系统中使用
man 2 epoll_create:查询
1.没有1024的限制,文件描述符上线可以通过配置文件修改
2.初始化文件描述符表(树)和返回的可操作文件描述符(数组)分离
3.不会循环检测已结束的文件描述符
int epoll_create(int size); { 创建一棵二叉树(树节点用来存放文件描述符) size:初始化树的节点个数,不是上限的意思
返回值:正确:返回树的操作句柄(整型)
错误:-1
}
文件描述符控制函数: epoll_ctl:实现树的增加,删除,修改
int epoll_ctl( int epfd, int op , int fd , struct epoll_event *event);
epfd :epoll_create 函数返回的树操作句柄
op:要对文件描述符执行的的操作
EPOLL_CTL_ADD:在树中添加文件描述符
EPOLL_CTL_MOD:修改文件描述符
EPOLL_CTL_DEL:删除文件描述符
fd:操作的文件描述符
event:一个结构体的地址(通过man手册查询)
//struct epoll_event en;
//en.events=EPOLLIN;
//en.date.fd=connfd;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events: 文件描述符需要的操作
例如:读:EPOLLIN
写:EPOLLOUT
异常:EPOLLERR
data是一个联合体:
通常使用第二个成员fd.代表需要检测的文件描述符
返回值: 正确返回 0 错误返回 -1
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_wait:用树来检测,检测到有操作的文件描述符时,就会给你将文件描述符 放在数组中,数组只是用来装一下文件描述符 (起到监听作用,前面的都是辅助的) int epoll_wait(int epfd, struct epoll_event*events,int maxevents,int timeout); { epfd: epoll_create 函数返回的树操作句柄 events: 结构体数组的首地址. maxevents: 数组中要操作的文件描述符最大个数.(一般和create函数传入的参数一致) timeout: -1: 代表一直等待直到有io操作才返回 0 :不等待立即返回 >0: 定时的时间 单位微秒 返回值: 正确返回 可操作的文件描述符个数 错误返回 -1 }
返回值:成功:返回监听列表里可以进行操作的文件描述符个数
IO多路复用总结: { 1.所有的IO多路复用机制(select,poll,epoll)都只能用于短作业,不能是复杂操作程序 原因:只有一个进程
select { 1.1024的限制 2.初始表与返回表未分离 3.会产生多余的循环次数 } poll { 1.突破1024的限制 2.初始表与返回表实现分离 3.会产生多余的循环次数 }
epoll { 1.突破1024的限制 2.初始表与返回表实现分离 3.不会产生多余的循环次数 } }
getsockopt setsockopt: 获取与设置套接字属性 { int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); { 获取套接字属性: sockfd: 套接字 level: level指定控制套接字的层次.可以取三种值: SOL_SOCKET: 通用套接字选项. 常用选项. IPPROTO_IP: IP选项. IPPROTO_TCP:TCP选项. optanme: 此参数,基本上都是使用内核提供的宏来完成设置. 例如: SO_REUSEADDR 代表允许重用本地地址和端口 optval: 此参数类型由第三个参数决定. 可以查看属性设置表来确定类型. 例如: SO_REUSEADDR 对应的类型 为 int optlen: 第四个参数的字节长度. 传出参数,所以传入地址. 返回值: 错误 -1 正确 0 } int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); { 设置套接字属性函数. 最后一个参数为传入参数. 所以传值. } int val = 1; 例如: setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
属性设置表:level
SOL_SOCKET ------------------------------------------------ 参数optname 宏的作用 对应参数optaval的类型
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
======================================================
}
超时检测(errno 代表错误号,可以在发生错误时打印) { 1.使用select 、poll、epoll这些函数提供参数来设置超时检测 2.使用setsockopt()来设置超时检测 设置监听套接字:如果设定时间内没人连接服务器,就会关闭 设置通信套接字:如果设定时间内客户端没有发消息,就会自动切断连接 }
广播与组播
{
都是使用UDP协议
广播:在局域网当中,发送端发送一条广播消息,其他主机都能收到
广播应用:在局域网中批量发送文件
屏幕共享软件VNC
电脑在自动获取IP地址时,也要先广播。
广播地址:主机号全为1,则表示一个广播地址 192.168.17.255
IP地址的分类:A B C D E
ipv4占四个字节:点分十进制“192.168.17.29”
二进制形式:1100 0000
A类:被大公司以及美国的一些高校使用
{
第一个字节的第一位只能以0开头
1.0.0.0126.255.255.255126.255.255.255:全0的地址表示任意匹配,127表示本地环回地址,所以不能私用
网络号:第一个字节 主机号:后三个字节
A类地址能分配的网络号有126个
在确定网络号A类地址能分配的主机号2^24-2 (全0不能使用,全1是)
}
B类: 能被个人pc使用.
{
第一个字节的前两位以10开头
128.0.0.0 ~ 191.255.255.255
网络号:前两个字节 主机号: 后两个字节.
B类地址能分配的网络号有 642^8 个
在确定网络号后:
B类地址能分配的主机号有 2^16-2 个 -2:全0不能使用 全1是广播地址也不能用
}
C类: 能被个人pc使用.
{
第一个字节的前三位以110开头
192.0.0.0 ~ 223.255.255.255
网络号:三个字节 主机号: 一个字节.
C类地址能分配的网络号有 322^16 个
在确定网络号后:
C类地址能分配的主机号有 2^8-2==254个 -2:全0不能使用 全1是广播地址也不能用
}
D类: 用于组播地址.
{
第一个字节以1110开头
224.0.0.0 ~ 239.255.255.255
}
子网掩码: 区分网络号与主机号.
通常形式: 网络号全为1 主机号全为 0 例如C类: 255.255.255.0
192.168.17.167/26: 26: 表示网络号的位数有26. 能分配的主机号:2^6 - 2
1.0.0.0
广播发送端 { 1.socket():SOCK_DGRAM 2.设置广播地址信息:struct sockaddr_in 3.设置广播发送权限setsockopt(); //如果想要接收端的端口不变化,就需要进行绑定操作(像路由器显示信息) //绑定的端口和ip地址都不能和广播地址,以及设置的广播端口相同 int val=1; setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&val,sizeof(val)) 允许发送广播数据 4sendto 5.close } 广播接收端(recv) { 1.socket() 2.设置广播地址信息 端口要和发送端对应 3.bind() 绑定,显示地址信息(广播地址)//想要收到别人的信息,必须绑定 4.recvfrom() 5.close }
项目二:文件服务器 opendir/readdir:读取当前目录下的文件 例:int mian() { DIR *p=opendir(".")//打开当前目录文件 struct dirent *dir; while(dir=readdir(p))//readdir 返回值:struct dirent 的指针,读完目录后返回NULL。 { if(dir->d_name[0] != ' . ')//过滤隐藏文件 { printf(”%s\n“,dir->d_name); } } return 0; } 服务器: recv循环读取:使recv返回0的方法(为了判断传输完成退出循环): 1.对方关闭套接字,在文件服务器中不这样做的原因是:每次传完一个文件后, 都需要对方关闭套接字,然后再重连,特别麻烦 2.对方先传文件大小,再传文件内容,recv的返回值每次进行累加, 当返回值累加到文件大小后,表示传输完成,退出循环。
为什么要用循环recv,不一次性读完: 不现实,读缓存区测试过,大约85KB左右,文件只要大于这个数值,recv读不完就阻塞了。 客户端: 1.接收终端输入的命令 2.判断命令执行相应操作 3.分别在子函数中创建套接字:不能直接在主函数中创建套接字的原因: 1)如果在主函数里面创建,子函数无法继续通信,
因为只有对方(服务器)把套接字关闭后,recv才会返回0
day6: 当地址被占用时,可以使用netstat -nap | grep "端口号",然后使用kill 杀死相应进程就好了。
组播发送端(UDP协议) { 1.创建数据报套接字 socket. SOCK_DGRAM.
2.设置组播地址信息. struct sockaddr_in. IP地址:设置组播的地址 端口:自己写 /*第三步可选,可以不用写*/ 3.设置组播属性. 用结构体struct ip_mreq 函数setsockopt(). 传参IP_MULTICAST_IF: (指定发送报文的接口,可以不用指定则会使用默认端口发送) struct ip_mreq { struct in_addr imr_multiaddr; /*组播ip地址*/ struct in_addr imr_interface; /*需要加入到组播的本地ip*/ }; struct ip_mreq mIp; mIp.imr_multiaddr.s_addr = inet_addr("239.10.0.1"); mIp.imr_interface.s_addr = inet_addr("192.168.17.29"); setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &mIp, sizeof(mIp)); 4.sendto() 5.close()}组播接收端{ 1.创建套接字 socket. SOCK_DGRAM. 2.设置接收端组播地址信息. struct sockaddr_in struct sockaddr_in (端口要和发送端设置的端口一致) 3.绑定bind. 显示地址信息,不然服务器找不到. 4.将当前地址加入到组播组中. 用结构体 struct ip_mreq 函数setsockopt(). IP_ADD_MEMBERSHIP struct ip_mreq mIp; mIp.imr_multiaddr.s_addr = inet_addr("239.10.0.1"); mIp.mr_interface.s_addr = inet_addr("192.168.17.29") setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP , &mIp, sizeof(mIp)); 5.接受数据. recvfrom(). 6.close()}
本地套接字 { 进程 { 1.有名管道 2.无名管道 3.信号 IPC { 4.共享内存 5.消息队列 6.信号灯 } BSD套接字 }
TCP或者UDP{ tcp本地服务器 { 1.socket(). AF_UNIX SOCK_STREAM 0 2.bind(). struct sockaddr_un; 3.listen(). 4.accept(). 数据收发 5.close(). } tcp本地客户端 { 1.socket(). AF_UNIX SOCK_STREAM 0 2.设置服务器地址信息 struct sockaddr_un; 3.connect() 数据收发 4.close(). }}
} 数据库(第三方软件) { 数据管理系统. sqlite数据库 { 1.全C语言写的, 3万行代码. 2.代码体积不大,嵌入式产品常用. 3.容量2TB左右 }
sqlite3数据库{ SQL语句. 数据库安装: dpkg -i: 离线安装 sudo dpkg -i *.deb 在线下载安装: sudo apt-get install sqlite3. sudo apt-get install libsqlite3-dev. 测试安装是否成功: 在终端输入 sqlite3 看是否能够进入到 数据库操作终端 创建: sqlite3 my.db (.db表示数据库文件) 基本指令 以.开头 指令后不能加 ; { .quit 退出数据库操作 .help 帮助文档 .tables 查看数据库中表的信息 .schema 查看创建表的详细信息. } 以 ; 结尾 开头不能有 . { 大小写都是可以的(SQL对大小写不敏感) create table <tablename>(<info1> <类型>, <info2> <类型>); 创建一张表 类型: text 文本类型(类似C语言字符串) int 整型 float 浮点型 增: insert { insert into <tablename> values(<info1>, <info2>); } 删: delete { delete from <tablename> where name="zhang san"; 按照名字删除 } 改: update { update student set grade=95 where name="li si"; } 查: select { select * from <tablename>; 从table里查找所有内容. select * from <tablename> order by <info>; 按照info从小到大的顺序查找. select * from <tablename> where <info> > 60; 按照info大于60值进行查找 } } fun(int **p) { *p = (int*)malloc(sizeof(int)); } int main() { int *q = NULL; *q = 4; fun(&q); }
C语言函数接口 { int sqlite3_open(char *path, sqlite3 **db); 功能:打开sqlite数据库
path: 数据库文件路径 db: 指向sqlite句柄的指针 返回值:成功返回0, 失败返回错误码(非零值) const char* sqlite3_errmsg(sqlite3 *db); 如果数据库的操作出现错误,则次函数会返回错误信息. 返回值:返回错误信息. int sqlite3_close(sqlite3 *db); 功能:关闭sqlite数据库 返回值:成功返回0,失败返回错误码 int sqlite3_exec(sqlite3 *db, const char *sql, sqlite3_callback callback, void *argv, char **errmsg); 功能:执行SQL操作 db:数据库句柄 sql:SQL语句 例如:sql="insert into stu values('yh', 59);" callback:回调函数 在对数据表进行查找时才用. 如果不用回调则传NULL argv: 传递给回调函数的参数. 如果不传参就用NULL errmsg:错误信息指针的地址. 返回值:成功返回0,失败返回错误码 typedef int (*sqlite3_callback)(void *para, int f_num, char **f_value, char **f_name); { para: sqlite3_exec 传递给回调函数的参数. f_num: 字段的数目. 例如: name num grade 表示三个字段 f_value: 包含每个字段值的指针数组 例如: char *p_val[] = {"et", "3", "45"}; f_name: 包含每个字段名的指针数组 例如: char *p_val[] = {"name", "num", "grade"}; 一定要: return 0; 返回值:成功返回0,失败返回-1 } 补充: 插入不存在的数据,存在则不插入: sql1 = "insert into user select '1234', 'qwer156' where not exists (select * from user where passwd='qwer123');";
}}
} 作业: 1.消化今天的内容 2.项目选做:1.udp聊天室 2.tftp文件服务器 3.英英词典翻译.