1、网络体系结构模型
2、网络编程专业术语:socket/ip/端口号
3、网络编程通信协议:TCP协议、UDP协议
4、网络编程IO模型:阻塞IO/非阻塞IO/多路复用/信号驱动
5、超时接受数据方法
一、系统编程中进程的通信方式
1、管道
无名管道(只能作用于亲缘间通信)---pipe write read
无名管道(系统中任意两个进程) ---mkfifo
2、信号
发送信号 ---kill
捕捉信号 ---signal
3、IPC对象
消息队列 ---带有数据标识的特殊管道 ftok() msgget msgsnd msgrcv msgctl
共享内存 ---双方进程可以同时对一片内存进行读写 ftok shmget shmat shmdt
信号量 ---不属于通信方式,只是一种互斥的方式 ftok semget semop semctl
特点:
只能在同一台主机上内部通信,不能跨平台
二、网络编程(也叫套接字(socket)编程)
1、特点
既可以在同一台主机上内部通信,还可以在不同主机之间通信。
自己的ubuntu ---- 自己的ubuntu
自己的ubuntu ---- 同一个局域网内除了自己之外的任意一台主机
总结: 网络通信的前提是 在同一个局域网内。
2、协议
在不同的主机之间通信,双方都必须遵顼的同一种规则
阿帕网(arpanet)
使用协议:网络控制协议(NCP)Network Control Program
缺点:不能互联不同类型的计算机 和 不同类型的操作系统,同时也没有纠错功能
因特网(Internet)
由于阿帕网(arpanet)的局限,所以在其基础上扩展 研发出了 因特网(Internet),并且引入了TCP/IP协议。
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。
TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。
三、计算机网络体系结构
1、概念
指的是 主机内部集成的结构和各层协议的集合。
每台主机本身就存在一个相同的网络体系结构。
2、作用
封装数据 和 解析数据
3、分类
ISO/OSI参考模型
TCP/IP参考模型
四、ISO/OSI参考模型 (七层)
1、概念
OSI从逻辑上,把一个网络系统分为功能上相对独立的7个有序的子系统,这样OSI体系结构就由功能上相对独立的7个层次组成,如下图所示。它们由低到高分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
应用层:确定这个数据应该以什么样的API接口进行处理
表示层:数据的压缩和解压、加密和解密这些工作都素由表示层完成的
会话层:访问验证,通信之间建立TCP会话
传输层:控制和检测 数据传输过程中是否出错,TCP协议、UDP协议
网络层:主要是IP协议,识别网络中不同的主机
数据链路层:1、将数据组合成数据块,也就是一帧帧的数据(给数据块添加数据头和数据尾)
物理层:承担数据传输的物理媒体(通信的通道),以太网接口
2、OSI参考模型 处理数据的效率很低,这个模型 已经被TCP/IP模型取消
补充:网路上的几个专有名词:数据、段、包、帧、比特
假如去除封装:最上层的数据,Data是应用层协议产生的数据,例如访问网页、看视频、听音乐,这些都可以称为应用层数据,电脑的操作系统会把这些应用层数据按照一定的规则传给下一层传输层。
在传输层,我们看到的数据称之为Segment,中文意思是段。在这一层,数据会被加上TCP或者UDP头,变成一个应用程序特有的数据。操作系统就是通过TCP或UDP端口号来区别不同应用程序的。
当数据再被往下传输的时候,就变成了packet,即“包”的意思。在这一层,Segment会被加上IP头部信息,然后就可以在三层传输了,而工作在三层的路由器会根据目的IP地址来转发这些”包“。
在往下,数据就会被加上MAC地址信息,名称就变成了Frame,”帧“。在这一层,就是交换机的世界了,交换机通常查找MAC地址表项来转发相应的”帧“。
mac地址百度百科链接:MAC地址_百度百科 (baidu.com)
网络中封包与拆包的过程:
五、TCP/IP协议模型(四层)
1、层次
应用层: Telnet FTP HTTP
传输层: TCP UDP
网际层: IP ICMP
网络接口层:以太网
2、每经过一层,会一层层添加头数据,当数据到达对方时,会一层层解析头数据
3、在上面模型中 ,传输层协议: TCP协议 、UDP协议(重点)
六、研究传输层协议
TCP协议(打电话)
1、概念
用来检测网络传输中差错的传输控制协议 (TCP,Transmission Control Protocol)。是一种面向连接的传输层协议,它能够提供高可靠性通信(也就是 数据 无误、数据无丢失、数据无失序、数据无重复到达的通信)
2、应用场合
1)对传输质量要求高、以及传输大量数据的通信
2)在需要可靠性数据传输的场合,通常使用TCP协议
3)QQ等 通信软件的用户登录账号管理 的功能,采用TCP
UDP(写信)
1、概念
用户数据报协议(UDP,User Datagram Protocol)。是一种不可靠、没有连接的协议。在数据发送之前,因为不需要进行连接,所以可以进行高效率的数据传输。
2、应用场合
1)发送小尺寸数据
2) 适用于 广播 /组播 通信
3)qq等通讯 软件中 点对点 文本通信 以及音视频
七、网络编程(套接字编程、socket编程)中几个重要概念
1、socket
插座、套接字 插座的种类比较多,就像很多个协议一样,必须提前设置好协议
1)是一个编程的函数接口,建立套接字
2)不管你是使用TCP协议 还是 用 UDP协议,首先必须使用socket进行确定
比如: 我们现在使用TCP协议
套接字文件描述符 socketfd = socket(TCP协议);
普通文件 : int fd = open(文件的名字);---》
3)套接字 是一种 特殊的 文件描述符
2、IP地址
1、概念
用来标识 网络中不同的主机。通信的前提必须要有一个IP地址
2、分类
IPV4地址 ---32位 IPV6地址 --128位
3、如何表示
点分十进制 "192.168.14.2" 网络号(网段 192.168.14) + 主机号(2)
4)数据包中 都必须要包含 目的IP地址、源IP地址。
3、端口号(无符号16位整数)
1、概念
标识 同一台主机内不同的应用程序。
2、字节序
1)、概念
一个多字节存储单元的低地址 存储数据的高有效位 还是 低有效位 ,说白了,也就是说 数据 在计算机内存中以什么样的方式存储
2)、分类
小端序:数据的低有效位存储 在 内存的 低地址
大端序:数据的低有效位存储 在 内存的 高地址
3)为了避免不同类别的主机之间 在数据交换时由于字节序的不同导致的差错,引入了网络字节序。也就是说统一规定所有的主机通过网络发送数据包时转为 大端序 ,也就是网络字节序。
网络字节序---》一定是大端序。
本地字节序---》取决于 主机 x86是小端 ARM是大端
总结: 无论当前你的主机是大端 还是 小端,都必须把自身的字节序 转成 网络字节序,即大端序,才能在网络中传输数据。
八、TCP通信过程
客户端通信:
1、买手机(建立套接字)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
函数作用:建立套接字
参数:
domain:你要选择哪一种地址族
PF_INET/AF_INET ------Ipv4 网络协议
PF_INET6/AF_INET6----- Ipv6 网络协议
type: 你要选择哪一种 协议(TCP UDP)
SOCK_STREAM --流式套接字 TCP
SOCK_DGRAM -数据报套接字 UDP
protocol:一般设置成 0
返回值:
成功返回 套接字文件描述符
失败 -1
2、绑定自己的电话号码(绑定自己的IP地址 和端口号)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
sockfd:套接字文件描述符
addr:自己的IP地址和端口号
addrlen:结构体的大小
返回值 :
成功则返回 0,
失败返回-1, 错误原因存于 errno 中
struct sockaddr --旧的结构体
{
unsigned short sa_family; /*地址族*/
char sa_data[14];/*14字节的协议地址,包含该socket的IP地址和端口号。*/
}; //IPV4结构体
struct sockaddr_in
{
short int sin_family; /*地址族 IPV4 IPV6*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/
};
struct in_addr {
in_addr_t s_addr; /*in_addr_t为 32位的unsigned int,该无符号整数采用大端字节序。*/
};
3、开始打电话(发起连接)
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address,socklen_t address_len);
参数:
sockfd:套接字文件描述符
address:对方的IP地址和端口号
address_len:结构体的大小
4、聊天
#include <sys/socket.h>
ssize_t send(int socket, const void *buffer, size_t length, int flags);
参数:
sockfd:套接字文件描述符
buffer:你要发送的数据
length:你要发送的数据大小 strlen
flags: 一般为0
返回值:
成功返回 发送的数据字节数
失败返回 -1
5、关闭
close(socketfd);
服务器 :
3、设置铃声(监听)
#include <sys/socket.h>
int listen(int socket, int backlog);
参数:
sockfd:套接字文件描述符
backlog:同时支持 最多的客户端连接个数
4、坐等电话(阻塞接收客户端的连接)
#include <sys/socket.h>
int accept(int socket, struct sockaddr * address,socklen_t * address_len);
参数:
sockfd:套接字文件描述符
address:存储已经连接上来的客户端的IP地址和端口号 ,如果不关系客户端的IP地址和端口号,可以设置成NULL
address_len:结构体的大小
返回值:
成功返回 已经连接上来的新的客户端的套接字文件描述符
失败返回 -1
扩展函数:
1、将本地IP地址转为网络IP地址
in_addr_t inet_addr(const char *cp);
参数:
cp:本地IP地址
返回值: 返回网络IP地址
一、扩展函数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
函数作用:
将本地IP地址转为网络IP地址
参数:
cp:本地IP地址
返回值:返回网络IP地址 ,注意是 in_addr_t
char *inet_ntoa(struct in_addr in); //n 网络 a 本地
函数作用:
将网络IP地址 转为本地IP地址
参数:
in:网络IP地址 注意是 struct in_addr 类型
返回值:
本地IP地址
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); //h表示本地 n表示网络 s表示 unsigned short int 16
函数作用:将本地端口号转为网络端口号
参数:
hostshort 本地端口号
返回值:
返回网络端口号
uint16_t ntohs(uint16_t netshort);
函数作用:将网络端口号转为本地端口号
参数:
netshort 网络端口号
返回值:
返回本地端口号
二、端口号复用
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int s, int level, int optname, const void * optval, , socklen_toptlen);
函数作用:设置端口号可以复用
参数:
s:套接字文件描述符
level 代表欲设置的网络层, 一般设成 SOL_SOCKET 以存取 socket 层
optname:SO_REUSEADDR 端口号复用
optval 代表欲设置的值, 比如给它一个1
optlen 则为 optval 的长度.
//所以设置端口号可以复用,这两条语句放在 绑定bind 之前
int optval = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval));
九、UDP协议
1、UDP协议中由于不需要连接,所以不能使用accept/connect函数。
2、UDP中是如何知道对方的地址???
在TCP中,先连接,所以connect(对方的IP地址和端口号)/accpet(客户端的IP地址和端口号)
在UDP中,地址和端口号 这些信号 只能与 数据 一起发送过去,所以接收的时候 地址和端口号 以及数据 一起接收
3、一定要区分 TCP 和 UDP 所使用的接口不一样,不要混淆了
TCP:
客户端:socket(SOCK_STREAM) bind connect send recv close
服务器:socket(SOCK_STREAM) bind listen accept recv send close
UDP:
客户端: socket(SOCK_DGRAM) bind sendto recvfrom close
服务器: socket(SOCK_DGRAM) bind recvfrom sendto close
十、UDP实现过程
客户端:
1、建立套接字(选择UDP协议)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
函数作用:建立套接字
参数:
domain:你要选择哪一种地址族
PF_INET/AF_INET ------Ipv4 网络协议
PF_INET6/AF_INET6----- Ipv6 网络协议
type: 你要选择哪一种 协议(TCP UDP)
SOCK_STREAM --流式套接字 TCP
SOCK_DGRAM -数据报套接字 UDP
protocol:一般设置成 0
返回值:
成功返回 套接字文件描述符
失败 -1
2、绑定自己的IP地址和端口号
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
sockfd:套接字文件描述符
addr:自己的IP地址和端口号
addrlen:结构体的大小
返回值 :
成功则返回 0,
失败返回-1, 错误原因存于 errno 中
struct sockaddr --旧的结构体
{
unsigned short sa_family; /*地址族*/
char sa_data[14];/*14字节的协议地址,包含该socket的IP地址和端口号。*/
};
//IPV4结构体
struct sockaddr_in
{
short int sin_family; /*地址族 IPV4 IPV6*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/
};
struct in_addr {
in_addr_t s_addr; /*in_addr_t为 32位的unsigned int,该无符号整数采用大端字节序。*/
};
3、直接发送(聊天)
#include <sys/socket.h>
ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);
函数作用: 用于UDP中发送数据,注意是UDP
参数:
socket :套接字文件描述符
message:你要发送的数据
length:你要发送的数据大小,注意有多少就发多少 strlen
flags:一般设置成 0
dest_addr:对方的IP地址和端口号
dest_len:结构体的大小
返回值:
成功返回 发送出去的字节数
失败返回 -1
4、关闭
close(socketfd);
服务器端:
#include <sys/socket.h>
ssize_t recvfrom(int socket, void * buffer, size_t length,int flags, struct sockaddr * address, socklen_t * address_len);
函数作用:用于UDP中接收数据
参数:
socket :套接字文件描述符
buffer:接收的数据存储在这里
length:接收的数据的大小, 以最大的来接收 sizeof()
flags:一般设置成 0
address: 存储 客户端的IP地址和端口号 ,可以获取到是 谁 给你 发送的
address_len:结构体的大小
返回值:
成功返回 接收到的字节数
失败返回 -1
十一、socket编程中四种IO模型
1、阻塞IO
1)read(fd) recv(socketfd) recvfrom(socketfd) ...
这些函数本身是不具有阻塞属性,而是因为这个文件描述符的本身阻塞属性导致这个函数执行表现出来的效果是阻塞。
2)在默认的情况下,linux中建立的套接字 、文件描述符 都是 阻塞的。
2、非阻塞IO
1)给文件描述符添加非阻塞属性。
2)由于非阻塞属性,所以可以不断地询问套接字中是否有数据到达。
3、多路复用
1)同时对多个IO口(文件描述符)进行操作(同时监听多个套接字)
2)可以在规定的时间内检测数据是否到达
4、信号驱动
1)属于异步通信方式
2)当socket中有数据到达时,通过发送信号通知用户
十二、阻塞IO
读阻塞:当数据缓冲区中没有数据可以读取时,调用 read recv recvfrom scanf 就会无限阻塞。
写阻塞:当数据缓冲区剩余的大小 小于 写入的数据量(你的空间不够写了),就会发生写阻塞,直到缓冲区的数据被读取。
例子: int fd = open("xxxx"); --->fd是阻塞。。
如果使用 O_NONBLOCK 该宏打开的 文件,那么文件描述符是非阻塞。
十三、非阻塞IO
1、阻塞IO 与 非阻塞IO之间的区别
阻塞IO
建立套接字(阻塞)----》读取数据---》判断缓冲区中没有数据
————没有数据--》进入一个等待的状态---》直到缓冲区中 有数据为止。
---来数据了---》读取数据 --》进入一个等待的状态---》直到缓冲区中 有数据为止。
非阻塞IO
建立套接字(阻塞)--》将这个套接字文件描述符的属性设置为非阻塞状态---》读取数据 --》
---没有数据---》读取失败---》接口会马上返回,不会阻塞
---》有数据 ---》读取成功--》接口也会返回
2、如何给文件描述符设置 非阻塞属性 ---file control fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
参数:
fd :你要设置哪个文件描述符,将这个文件描述符传递进来
cmd:控制的命令
arg:由第二个参数进行决定
第二个参数cmd:
F_GETFL: Get the file status flags and file access modes, 可以得到文件描述符的状态 ,第三个参数可以忽略
F_SETFL: 设置文件描述符的状态,但只允许 O_APPEND、O_NONBLOCK 和O_ASYNC 位的改变, 其他位的改变将不受影响.
O_APPEND: 可以设置成 追加
O_NONBLOCK:可以设置 非阻塞属性
返回值 :
F_GETFL --》返回 文件描述符的状态或者属性
F_SETFL -->
成功返回 0
失败返回 -1
比如:
int socketfd = socket(TCP协议);//默认是阻塞的
//得到这个套接字文件描述符的属性
int status = fcntl(socketfd,F_GETFL)
//将得到的文件描述符的全部属性 中的 其中一个属性设置成 非阻塞
status |= O_NONBLOCK;
fcntl(socketfd,F_SETFL,status);//把变量status的状态设置到文件描述符中
思考:为什么系统 可以使用 位或的方式 将其中一个属性设置到一个变量里面,系统就可以知道呢?????
0000 1100 1100 0110
1000
O_NONBLOCK
1000
3、例子
设置一个非阻塞属性 给套接字,看看这个套接字 是否 还会阻塞等待客户端的连接。。
=============
#include<stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#define OWNADDR "192.168.14.3" //我自己电脑的ip地址
#define OWNPORT 20000 //我自己电脑的该程序的端口号
int main()
{
printf("当前是服务器 IP:%s Port:%u\n",OWNADDR,OWNPORT);
//1、买手机(建立套接字)
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
if(socketfd == -1)
{
printf("没钱了....,失败\n");
return -1;
}
//因为服务器立马退出之后,端口号不会及时释放
//此时如果服务器又马上运行,那么端口号会被占用,导致服务器分配端口号失败,连接失败
//所以设置端口号可以复用
int optval = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval));
//2、绑定自己的电话号码(绑定自己的IP地址 和端口号)
//定义一个IPV4结构体变量,初始化自己的IP地址和端口号
struct sockaddr_in ownAddr;
ownAddr.sin_family = AF_INET;/*地址族 IPV4*/
ownAddr.sin_port = htons(OWNPORT); //htons 将本地端口号转为网络端口号
ownAddr.sin_addr.s_addr = inet_addr(OWNADDR); //将本地IP地址转为网络IP地址
bind(socketfd, (struct sockaddr *)&ownAddr,sizeof(struct sockaddr_in));
//3、设置铃声(监听) listen
listen(socketfd,5);
//4、坐等电话(阻塞接收客户端的连接)
printf("等待客户端连接.......\n");
//定义一个IPV4结构体变量,存储连接上来的客户端的IP地址 和 端口号
struct sockaddr_in clientAddr;
//如果你要想要获取对方的IP地址和端口号,第三个参数必须把结构体的大小传递进去
int len = sizeof(struct sockaddr_in);
//给 socketfd设置非阻塞属性
int status = fcntl(socketfd,F_GETFL);//得到这个套接字文件描述符的属性
//将得到的文件描述符的全部属性 中的 其中一个属性设置成 非阻塞
status |= O_NONBLOCK;
int ret = fcntl(socketfd,F_SETFL,status);//把变量status的状态设置到文件描述符中
if(ret == -1)
{
printf("fcntl error\n");
}
while(1)
{
//此时是非阻塞,会一直不断地循环
int newClientFd = accept(socketfd,(struct sockaddr*)&clientAddr,&len);
if(newClientFd != -1)
{
//printf("有客户端连接上来了............\n");
//打印连接上来的客户端的IP地址和端口号,将网络字节序转为 本地字节序
printf("连接上来的客户端IP:%s 端口号:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
}
//printf("11111111111\n");
}
//5、关闭
close(socketfd);
//close(newClientFd);
return 0;
{
设置一个非阻塞属性 给套接字,看看这个套接字 是否 还会阻塞等待客户端的连接......
步骤:
1、建立套接字
2、//因为服务器立马退出之后,端口号不会及时释放
//此时如果服务器又马上运行,那么端口号会被占用,导致服务器分配端口号失败,连接失败
//所以设置端口号可以复用
3、绑定自己的IP地址 和端口号
//定义一个IPV4结构体变量,初始化自己的IP地址和端口号
4、设置铃声(监听) listen
5、阻塞接收客户端的连接
6、关闭
}
十四、多路复用(重点)
1、同时监听多个套接字
阻塞IO---->只能同时监听一个套接字
非阻塞IO--->一直轮询,询问IO口有没有数据到来,非常浪费CPU资源
2、什么是多路复用
就是预先把需要监听的文件描述符加入到一个集合中,然后在规定的时间内 或者 无限时间阻塞等待。
如果在规定的时间内,集合中文件描述符没有数据变化,则说明超时接收,会进入下一次规定的时间内再次等待。
一旦集合中的文件描述符有数据变化,则其他没有数据变化的文件描述符就会被踢除到集合之外,并且会再次进入下一次的等待状态。
3、特点
同时监听多个套接字
4、多路复用的函数接口 ---select
/* 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);
参数:
nfds: 最大的文件描述符+1
readfds: 监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者说 读取是不是不阻塞 99%
writefds:监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写 ,如果不想监视可写,可设置为NULL
exceptfds: 用来监视发生错误 异常 一般设置为NULL
timeout: 设置最大的等待时间--》超时一次之后,需要重新设置一个时间值,再传递给select这个函数
--如果为NULL,则说明这个函数是阻塞(无限等待,直到有数据到来)
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
};
比如设置5秒
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
1秒 = 1000 000 微秒
返回值:
成功
有数据到达 ---》就绪的文件描述符的总数
没有数据到达 --》超时--》0
失败
-1
5、关于文件描述符集合的函数接口
fd_set---》文件描述符集合 数据类型
1)删除集合中某个文件描述符fd
void FD_CLR(int fd, fd_set *set);
2)判断某个文件描述符 fd 是否 在集合set中
int FD_ISSET(int fd, fd_set *set);
3)将文件描述符fd加入到集合中set
void FD_SET(int fd, fd_set *set);
4)清空这个集合set
void FD_ZERO(fd_set *set);
例子: 实现客户端 与 服务器 之间 进行收发数据,在5秒内必须有数据到达,否则超时。
客户端 服务器
"hai" ---> "hai"
"zouni" <---- "zouni"
使用select同时监听客户端两个文件描述符 使用select同时监听服务器两个文件描述符
标准输入(STDIN_FILENO) 标准输入(STDIN_FILENO)
socketfd newClientFd
客户端:
#include<stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define OWNADDR "192.168.14.3" //我自己ubuntu的ip地址
#define OWNPORT 10000 //我自己ubuntu的该程序的端口号
#define SERVERADDR "192.168.14.3" //对方的 服务器的IP地址
#define SERVERPORT 20000 //对方的 服务器的端口号
//TCP客户端
int main()
{
printf("TCP客户端 IP:%s Port:%u\n",OWNADDR,OWNPORT);
//1、买手机(建立套接字)
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
perror("socket error");
return -1;
}
//2、可以不用绑定
//3、发起连接
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;/*地址族 IPV4*/
serverAddr.sin_port = htons(SERVERPORT); //htons 将本地端口号转为网络端口号
serverAddr.sin_addr.s_addr = inet_addr(SERVERADDR); //将本地IP地址转为网络IP地址
connect(socketfd,(struct sockaddr *)&serverAddr,sizeof(struct sockaddr_in));
//通过比较,获取最大的文件描述符
int maxfd = STDIN_FILENO > socketfd ? STDIN_FILENO:socketfd;
struct timeval timeout;
while(1)
{
//一定要注意:集合的初始化一定放在循环里面
fd_set set;//定义一个文件描述符集合
//清空这个集合set
FD_ZERO(&set);
//将标准输入添加到集合中
FD_SET(STDIN_FILENO,&set);
//将套接字文件描述符添加到集合中
FD_SET(socketfd,&set);
//时间的设置也要放到循环里面
timeout.tv_sec = 5;//设置5秒
timeout.tv_usec = 0;
//使用多路复用的方式同时监听 标准输入STDIN_FILENO 和 socketfd
int ret = select(maxfd+1, &set,NULL,NULL, &timeout);
if(ret == -1)
{
printf("select error\n");
break;
}
else if(ret == 0)//超时了
{
printf("timeout......\n");
continue;//如果超时了,那么回到循环开头 又重新开始 监视
}
//代码如果执行到下面,说明文件描述符有变化了
//那么,到底是哪个文件描述符有变化了
//1)如果是键盘有输入,那么标准输入就有变化
if(FD_ISSET(STDIN_FILENO, &set))
{
//可以从键盘中获取数据了,然后发送给服务器
char buf[1024]={0};
scanf("%s",buf);
//发送
send(socketfd, buf, strlen(buf), 0);
}
//2)如果是服务器发送数据过来,那么socketfd就会变化
if(FD_ISSET(socketfd, &set))
{
char buf[1024]={0};
int ret = recv(socketfd,buf,sizeof(buf),0);
if(ret == 0)
break;
printf("recv:%s\n",buf);
}
}
return 0;
}
服务器端:
#include<stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define OWNADDR "192.168.14.3" //我自己ubuntu的ip地址
#define OWNPORT 20000 //我自己ubuntu的该程序的端口号
//TCP服务器
int main()
{
printf("TCP服务器 IP:%s Port:%u\n",OWNADDR,OWNPORT);
//1、买手机(建立套接字)
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
perror("socket error");
return -1;
}
//因为服务器立马退出之后,端口号不会及时释放
//此时如果服务器又马上运行,那么端口号会被占用,导致服务器分配端口号失败,连接失败
//所以设置端口号可以复用
int optval = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval));
//2、绑定自己的电话号码(绑定自己的IP地址 和端口号)
//定义一个IPV4结构体变量,初始化自己的IP地址和端口号
struct sockaddr_in ownAddr;
ownAddr.sin_family = AF_INET;/*地址族 IPV4*/
ownAddr.sin_port = htons(OWNPORT); //htons 将本地端口号转为网络端口号
ownAddr.sin_addr.s_addr = inet_addr(OWNADDR); //将本地IP地址转为网络IP地址
bind(socketfd, (struct sockaddr *)&ownAddr,sizeof(struct sockaddr_in));
//3、设置铃声(监听) listen
listen(socketfd,5);
//4、坐等电话(阻塞接收客户端的连接)
printf("等待客户端连接.......\n");
//定义一个IPV4结构体变量,存储连接上来的客户端的IP地址 和 端口号
struct sockaddr_in clientAddr;
//如果你要想要获取对方的IP地址和端口号,第三个参数必须把结构体的大小传递进去
int len = sizeof(struct sockaddr_in);
int newClientFd = accept(socketfd,(struct sockaddr*)&clientAddr,&len);
if(newClientFd != -1)
{
printf("有客户端连接上来了............\n");
//打印连接上来的客户端的IP地址和端口号,将网络字节序转为 本地字节序
printf("连接上来的客户端IP:%s 端口号:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
}
//通过比较,获取最大的文件描述符
int maxfd = STDIN_FILENO > newClientFd ? STDIN_FILENO:newClientFd;
struct timeval timeout;
while(1)
{
//一定要注意:集合的初始化一定放在循环里面
fd_set set;//定义一个文件描述符集合
//清空这个集合set
FD_ZERO(&set);
//将标准输入添加到集合中
FD_SET(STDIN_FILENO,&set);
//将套接字文件描述符添加到集合中
FD_SET(newClientFd,&set);
//时间的设置也要放到循环里面
timeout.tv_sec = 5;//设置5秒
timeout.tv_usec = 0;
//使用多路复用的方式同时监听 标准输入STDIN_FILENO 和 newClientFd
int ret = select(maxfd+1, &set,NULL,NULL, &timeout);
if(ret == -1)
{
printf("select error\n");
break;
}
else if(ret == 0)//超时了
{
printf("timeout......\n");
continue;
}
//代码如果执行到下面,说明文件描述符有变化了
//那么,到底是哪个文件描述符有变化了
//1)如果是键盘有输入,那么标准输入就有变化
if(FD_ISSET(STDIN_FILENO, &set))
{
//可以从键盘中获取数据了,然后发送给客户端
char buf[1024]={0};
scanf("%s",buf);
//发送
send(newClientFd, buf, strlen(buf), 0);
}
//2)如果是客户端发送数据过来,那么newClientFd就会变化
if(FD_ISSET(newClientFd, &set))
{
char buf[1024]={0};
int ret = recv(newClientFd,buf,sizeof(buf),0);
if(ret == 0)
break;
printf("recv:%s\n",buf);
}
}
//5、关闭
close(socketfd);
close(newClientFd);
return 0;
}
二、信号驱动
1、信号驱动原理是怎样??
使用了系统编程中信号的机制,首先让程序安装SIGIO信号处理函数,通过监听文件描述符是否产生了SIGIO信号
我们就知道了文件描述符有没有数据到达。如果有数据到达(你朋友来了),则系统就会自动产生一个SIGIO信号(门铃响了),
我们只需要在信号响应函数中读取数据就可以了
//信号响应函数
void my_fun(int arg)
{
}
signal(SIGIO,my_fun);
2、信号驱动特点以及步骤????
特点:适用于UDP协议,不适用TCP协议。
1)由于数据不知道什么时候到达,我们是不知道的,所以我们需要设置一个信号响应函数,捕捉这个信号。
2)设置套接字的属主。--->告诉系统 这个套接字 对应的 进程ID是多少。实际上就是 将 这个套接字 和 进程ID号绑定起来
3)如果使用信号驱动,则需要给这个套接字添加 信号触发模式 属性。因为默认是没有的
3、如何设置套接字的属主 ---》fcntl
F_SETOWN --->用于设置属主
fcntl(socketfd,F_SETOWN, getpid());
4、如何给套接字 添加 信号触发模式属性
//得到这个套接字文件描述符的属性
int status = fcntl(socketfd,F_GETFL);
//将得到的文件描述符的全部属性 中的 其中一个属性设置成 信号触发模式
status |= O_ASYNC;
int ret = fcntl(socketfd,F_SETFL,status);//把变量status的状态设置到文件描述符中
十五、广播与组播
一、广播
1、什么是广播
单播:数据包发送方式只有一个接受方
广播:同时发给局域网中的所有主机
2、特点
只有用户数据报套接字(使用UDP协议)才能广播
3、广播地址
以192.168.14.0网段为例:***.***.***.255 代表该网段的广播地址。发送给该地址的数据包被所有主机接收
比如我们当前教室的局域网段 是 192.168.14.0 ,那么广播地址就是 192.168.14.255
sendto("你好",192.168.14.255)
4、实现广播的过程(一定是使用UDP协议)
广播发送端:
1、创建数据报套接字 UDP
int socketfd = socket(AF_INET,SOCK_DGRAM,0);
2、设置socketfd套接字文件描述符的属性为 广播 。(也就是允许发送广播数据包)
SO_BROADCAST -----》使用广播方式传送
int on=1;
setsockopt(sockfd , SOL_SOCKET,SO_BROADCAST,&on, sizeof(on));
3、发送数据 ,指定接收方为广播地址
struct sockaddr_in sendAddr;
sendAddr.sin_family = AF_INET;
sendAddr.sin_port = htons(10000);
sendAddr.sin_addr.s_addr = inet_addr("192.168.14.255");//一定是广播地址,广播发送
sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&sendAddr,sizeof(sendAddr));
4、关闭
广播接收方:
1、创建用户数据报套接字
int socketfd = socket(AF_INET,SOCK_DGRAM,0);
2、绑定(192.168.14.255)广播IP地址和端口号 (10000)
注意:绑定的端口必须和发送方指定的端口相同
struct sockaddr_in ownAddr;
ownAddr.sin_family = AF_INET;
ownAddr.sin_port = htons(10000);
//uint32_t htonl(uint32_t hostlong); 将 主机IP转为 网络IP
ownAddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY(0.0.0.0) 代表本机所有的地址 //inet_addr("192.168.14.255");
3、接收数据
recvfrom(。。。。);
扩展函数:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h:host 代表主机
to: 代表 转换的意思
n: network 代表 网络
l:unsigned long int(64位下 8个字节 32位 下 4个字节) 无符号32位 代表 IP地址
s: unsigned short int 无符号16位 代表 端口号
练习1: 把广播通信进行实现。
发送端:
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main()
{
printf("广播发送端(UDP协议)...............\n");
//1、创建数据报套接字
int socketfd = socket(AF_INET,SOCK_DGRAM,0);
if(socketfd == -1)
{
perror("socket error");
return -1;
}
//2、设置套接字文件描述符socketfd的属性为广播(也就是允许发送广播数据包)
int on=1;
setsockopt(socketfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
//3、发送数据,并且指定接收方为广播地址
struct sockaddr_in sendAddr;//IPV4地址结构体变量
sendAddr.sin_family = AF_INET;
sendAddr.sin_port = htons(10000);
sendAddr.sin_addr.s_addr = inet_addr("192.168.14.255");//一定是广播地址,广播发送
while(1)
{
char buf[1024]={0};
printf("data:");
scanf("%s",buf); //100+"hello"
sendto(socketfd,buf,strlen(buf),0,( struct sockaddr *)&sendAddr,sizeof(sendAddr));
}
close(socketfd);
return 0;
}
接收端:
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main()
{
printf("广播接收端(UDP协议)...............\n");
//1、创建数据报套接字
int socketfd = socket(AF_INET,SOCK_DGRAM,0);
if(socketfd == -1)
{
perror("socket error");
return -1;
}
//2、绑定(192.168.14.255)广播IP地址和端口号 (10000)
struct sockaddr_in ownAddr;
ownAddr.sin_family = AF_INET;
ownAddr.sin_port = htons(10000); //s unsigned short int
ownAddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY(0.0.0.0) 代表本机所有的地址 //inet_addr("192.168.14.255");
bind(socketfd, (struct sockaddr *)&ownAddr,sizeof(ownAddr));
struct sockaddr_in otherAddr;
int len = sizeof(struct sockaddr_in);
while(1)
{
char buf[1024]={0};
recvfrom(socketfd,buf,sizeof(buf),0, (struct sockaddr *)&otherAddr,&len);
printf("来自 %s:%u recv:%s\n",inet_ntoa(otherAddr.sin_addr),ntohs(otherAddr.sin_port),buf);
}
close(socketfd);
return 0;
}
二、组播(群聊)
1、概念
组播是介于单播与广播之间,在一个局域网内,将某些主机添加到组中,并设置一个组地址
我们只需要将数据发送到组播地址即可,加入到该组的所有主机都能接收到数据
2、组播特点
1.需要给组播设置IP地址,该IP必须是D类地址
2.只有UDP才能设置组播
3、IP地址分类
IP地址 = 网络号 + 主机号
网络号:指的是不同的网络
主机号:指的是同一个网段下用来识别不同的主机。那也就是说,主机号所占的位数越多,在该网段下的主机数越多。
A类地址 :保留给政府机构使用
A类IP地址就由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”
A类地址范围 1.0.0.1 - 126.255.255.254
B类地址 :分配给中等规模的公司
B类IP地址就由2字节的网络地址和2字节主机地址组成,网络地址的最高位必须是“10”。
B类地址范围 128.0.0.1 - 191.255.255.254
C类地址 :分配给任何需要的人
C类IP地址就由3字节的网络地址和1字节主机地址组成,网络地址的最高位必须是“110”。
C类地址范围 192.0.0.1 - 223.255.255.254 //192.168.14.2
D类地址 :用于组播
D类地址范围 224.0.0.1 - 239.255.255.254 //224.0.0.10
E类地址 :用于实验
E类地址范围 240.0.0.1 - 255.255.255.254
特殊地址:
每一个字节都为0的地址(“0.0.0.0”)对应于当前主机; INADDR_ANY -->代表当前主机所有的地址
127.0.0.1 回环地址 --》在当前主机内部自动形成闭环的网络 --》主要用于 主机内部不同的应用程序通信
如果你已经确定当前客户端 和 服务器 都是在同一台主机上运行,那么可以使用这个地址
4、接收端怎么接收组播消息? -->需要加入组播属性的套接字
#define IP_ADD_MEMBERSHIP 加入组播
// usr/include/linux/in.h
struct ip_mreq {
struct in_addr imr_multiaddr; /* 多播组的IP地址 224.0.0.10/
struct in_addr imr_interface; /* 需要加入到多组的IP地址 192.168.53.134 */
};
5、组播通信的过程
发送端:
1、创建UDP数据报套接字
2、发送数据,往组播地址(224.0.0.10 )里面发送数据
3、关闭
接收端:(要把接收端的IP地址加入到组播里面)
1、创建UDP数据报套接字
2、定义组播结构体
struct ip_mreq vmreq;
3、设置组播ip(初始化 组播结构体)
inet_pton(AF_INET,"224.0.0.10",&vmreq.imr_multiaddr); // 组播地址
inet_pton(AF_INET,"192.168.14.3",&vmreq.imr_interface); // 需要添加到组的ip
#ininclude <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数:
af : 你要选择哪一种协议族 IPV4 --》AF_INET 还是 IPV6--》AF_INET6
src:本地IP地址
dst:将本地IP地址转为网络IP地址存储到这里
作用:
将本地IP地址转为网络IP地址
4)加入组播属性(也就是设置这个套接字 可以接收组播信息)
setsockopt(socketfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&vmreq,sizeof(vmreq));
5)绑定地址
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl(INADDR_ANY) 代表 主机所有的地址
bind(socketfd,(struct sockaddr *)&saddr,sizeof(saddr));
6)接收数据
recvfrom(......)