linux网络编程

一、网络基础概念

1.1 协议

一组规则,双方规定一些约束

概念

协议事先约定好, 大家共同遵守的一组规则,如交通信号灯。从应用程序的角度看, 协议可理解为数据传输和数据解释的规则;可以简单的理解为各个主机之间进行通信所使用的共同语言。

假设,A、B双方欲传输文件。规定:

第一次: 传输文件名,接收方接收到文件名,应答OK给传输方;

第二次: 发送文件的尺寸,接收方接收到该数据再次应答一个OK;

第三次: 传输文件内容。同样,接收方接收数据完成后应答OK表示文件内容接收成功。

在这里插入图片描述

​ 这种在A和B之间被遵守的协议称之为原始协议,后来经过不断增加完善改进,最终形成了一个稳定的完整的传输协议,被广泛应用于各种文件传输,该协议逐渐就成了一个标准协议。

几种常见的协议

应用层 :常见的协议有HTTP协议,FTP协议,NFS协议,SSH协议,SMTP协议。
传输层 :常见协议有TCP/UDP协议。
网络层 :常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层 :常见协议有ARP协议、RARP协议。

//4.应用层:
HTTP超文本传输协议(Hyper Text Transfer Protocol):是互联网上应用最为广泛的一种网络协议。
FTP文件传输协议(File Transfer Protocol)
NFS(网络挂载协议)
SSH(远程登陆协议)
SMTP(简单邮件传输协议)
    
//3.传输层:
TCP传输控制协议(Transmission Control Protocol):是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议(User Datagram Protocol):是一种无连接的,基于数据报的一种传输层通信协议,不保证数据安全可靠。
SCTP(流控制传输协议)

//2.网络层:
IP协议:是因特网互联协议(Internet Protocol)
ICMP协议:是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议:是Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议:是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP是:反向地址转换协议,通过MAC地址确定IP地址。

//1.物理与网络接口层:
以太网协议

1.2 网络协议模型

1.2.1 OSI七层模型

OSIOpen System Interconnection的缩写, 意为开放式系统互联国际标准化组织(ISO)制定了OSI模型,该模型定义了不同计算机互联的标准, 是设计和描述计算机网络通信的基本框架。

网络分层 OSI 7层模型: **物数网传会表应 **

​ Ø 物理层—双绞线,光纤(传输介质),将模拟信号转换为数字信号。

​ Ø 数据链路层—数据校验,定义了网络传输的基本单位-帧 。

​ Ø 网络层—定义网络,两台机器之间传输的路径选择点到点的传输。

​ Ø 传输层—传输数据 TCP,UDP,端到端的传输。

​ Ø 会话层—通过传输层建立数据传输的通道。

​ Ø 表示层—编解码,翻译工作。

​ Ø 应用层—为客户提供各种应用服务,email服务,ftp服务,ssh服务

在这里插入图片描述

1.2.2 TCP/IP四层模型

TCP/IP网络协议族分为

  • 应用层(Application)
  • 传输层(Transport)
  • 网络层(Network)
  • 物理和网络接口层

在这里插入图片描述

一般讨论最多的是TCP/IP模型。

1.3 网络通信原理

其实就是发送端层层打包,接收端层层解包

交换机

  • 一种网络硬件,通过报文交换接收和转发数据到目标设备,它能够在计算机网络上连接不同的设备,一般也简称为交换机。
  • 二层交换机工作于OSI参考模型的第二层,即数据链路层。
  • 交换机的原理(二层):
    1. A主机封装好网络数据包(包含地址信息和数据),将数据发送给交换机。
    2. 交换机解析目标mac地址信息。
    3. 交换机查mac地址和端口映射表,找到目标地址对应的端口。
      • 若没有找到对应的端口,则会将该数据报广播给所有连接的端口(除了自己),此现象被称为“泛洪”。
      • 注:主机主动发消息给交换机时,交换机才会记录主机的mac与自己的端口映射起来。
    4. 交换机通过目标端口将数据转发出去,到达B主机。
    5. ARP:根据ip地址找到对应的mac地址。
    6. RARP:用于将mac地址转换为ip地址。

路由器

  • 一种电讯网络设备,用于不同子网(不同局域网)的数据交换(路由),规划路径。
  • 一种电讯网络设备,提供路由与转送两种重要机制。
    1. 可以决定数据包由来源端到目的端所经过的路径(host 到 host之间的传输路径),这个过程称为路由。
    2. 将路由器输入端的数据包转送至适合的路由输出端(在路由器内部进行),这称为转送。
  • 路由工作在OSI模型的第三层,例如网间协议(IP),可实现网关功能。
  • 包含两个IP地址:
    1. 对应LAN口——内网ip
    2. 对应WAN口——外网ip(公网ip)

路由器与交换机的区别

  1. 路由器是OSI第三层的产品,而交换机则是第二层的产品,第二层主要功能是将网络上各个电脑的MAC地址记在MAC地址表中,当局域网中的电脑要经过交换机去交换传递资料时,就查询交换机上的MAC地址表中的信息,并将数据包发送给指定的电脑,而不会像第一层的产品(如集线器)发送给每台在网络中的电脑。
  2. 路由器能在多条路径中选择最佳的路径,提升交换数据的传输速率,在发送数据包时,路由器会被一同发送,该表存储了前往某一网络的最佳路线,如该路径的“路由度量值”,参考路由表可获得这个过程的详细描述。
  3. 路由器可连接超过两个以上不同的设备,二二层交换机只能连接两个。
  4. 路由器具有IP分享器功能,主要是让多台设备用同一条ADSL/光纤宽带线路来上网,功能包括共享IP,宽带管理,自动分配IP等等。

二、tcp通信

2.1 套接字

socket其实是在“应用层与传输层之间的产物”,将传输层的很多复杂操作封装成一些简单的接口,来让应用层调用,以此来实现进程在网络中的通信。

本质上是一个非负整数,是一种特殊的文件描述符

如何创建套接字?

int socket(int domain,int type,int protocol);
//@ret : 成功返回监听套接字,失败返回-1
/*@param:
		domain:		协议族 
				AF_UNIX/AF_LOCAL	用于本地通信
				AF_INET			选择Ipv4协议(网际协议)
				AF_INET6		选择IPV6协议(互联网协议)
		
		type:		创建套接字类型
			SOCK_STREM	流式套接字,对应TCP与SCTP协议
						数据传输是一个字节一个字节传输
			SOCK_DGRAM	数据套接字,对应UDP通信,数据传输是一个数据报一个数据报的传输,每个数据报包含目的地址和其他数据之间的关系
			SOCK_SEQPACKET	有序分组套接字,对应SCTP协议
			SOCK_RAW	原始套接字,跳过传输层
		
		ptotocol:	协议类型
				0表示默认方法
				IPPROTO_TCP	
				IPPROTO_UDP
				IPPROTO_SCTP
			只有当第一个参数为AF_INET6时且使用SCTP协议时才需要配置,其他情况下配0即可
*/

2.2 IP与端口

IP地址:唯一标识一台计算机

port:端口号,是一个unsigned short 类型的无符号整数

  • 0~1023 系统端口
  • 1025 ~ 5000 应用程序借口
  • 5001 ~ 65535 用户预留接口

2.3 TCP通信流程

2.3.1 服务器(server)

//1.创建套接字----返回套接字,失败返回-1
	int socket(int domain, int type, int protocol);
	//	协议簇		套接字类型	一般设置0

//2.绑定本机IP地址与端口号----成功返回0,失败返回-1
	int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	//	sockfd:	监听的套接字
	//	addr:	对应协议服务器地址结构的首地址
	//	addrlen:地址结构的大小

//3.设置监听套接字----成功返回0,失败返回-1
	int listen(int sockfd, int backlog);
	//	sockfd:	监听的套接字
	//	backlog: 等待连接的最大队列长度

//4.等待客户端连接----成功返回通信套接字(读写套接字),失败返回-1
	int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	//	addr: 客户端的地址结构的首地址
	//	addlen: 客户端地址结构的大小的首地址
	//注:若不想接受客户端的信息,addr与addelen两个参数填NULL

//5.数据发送接收
	read()/write()------recv()/send()
//6.关闭套接字断开连接
	close()

demo:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <arpa/inet.h>

#define DEF_VAL 0
#define ERR_VAL -1
#define ERR_LOG(val) do{perror(val);exit(EXIT_FAILURE);}while(0)

#define MAX 1024
#define SERVER_IP "192.168.6.130"

int main(int argc, const char *argv[])
{
	//判断命令行参数是否过少
	if(argc < 2)
	{
		printf("less file!\n");
        exit(1);
	}

	/* 	argv是char **类型,命令行写入是字符串,
		int atoi(const char *nptr) 将字符串转换成int型	*/
	int port = atoi(argv[1]);

	//1.创建套接字
	//int socket(int domain, int type, int protocol);
	int sockfd;
	sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd == -1)
	{
		ERR_LOG("socket");
	}
	printf("====创建套接字成功,sockfd = %d\n",sockfd);

	//socket地址结构体,成员分别为:ip协议组、端口号以及ip地址
	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family = AF_INET;
	//saddr.sin_port = htons(8887);
	saddr.sin_port = htons(port);  //从命令行中传入端口参数

	//转换小端点分十进制为大端二进制
	int ret = inet_pton(AF_INET,SERVER_IP,&saddr.sin_addr);
	if (ret == ERR_VAL)
	{
		ERR_LOG("inet_pton");
	}

	//2.绑定IP地址和端口号
	//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	ret = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
	if(ret == ERR_VAL)
	{
		ERR_LOG("bind");
	}
	printf("====绑定地址成功!\n");

	//3.设置监听套接字
	//int listen(int sockfd, int backlog);
	ret = listen(sockfd,5);//将套接字改为被动套接字
	if(ret == ERR_VAL)
	{
		ERR_LOG("listen");
	}
	printf("====监听成功!\n");

	//printf("waiting for the connection...\n");
	printf("等待客户端连接...\n");
	//4.等待客户端连接
	//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	int connfd = accept(sockfd,NULL,NULL); //阻塞等待,匿名接收。
	if( connfd == ERR_VAL )
	{
		ERR_LOG("accept!\n");
	}
	printf("====连接成功!\n");

	char buf[MAX] = {0};
	int n = 0;
	//5.数据的发送接收
	while(1)
	{
		//读客户端发来的消息
		n = read(connfd,buf,sizeof(buf));
		if(n <= DEF_VAL)
		{
			printf("客户端退出!\n");
			break;
		}
		//将客户端发来的消息回发给客户端
		write(connfd,buf,n);
		printf("客户端发来的消息:%s",buf);
		memset(buf,0,sizeof(buf));
	}

	//6.文件套接字与监听套接字断开连接
	close(sockfd);
	close(connfd);

	return 0;
}

2.3.2 客户端(client)

//1.创建套接字
//2.绑定本机IP地址与端口号(此步可省略,内核帮做此步骤)
//3.向服务器发起连接
//4.数据发送、接收
//关闭套接字断开连接

demo:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>

#define DEF_VAL 0
#define ERR_VAL -1
#define ERR_LOG(val) do{perror(val);exit(EXIT_FAILURE);}while(0)

#define MAX 1024

int main(int argc, const char *argv[])
{
	//判断命令行参数是否过少
	if(argc < 2)
	{
		ERR_LOG("less fail!\n");
	}

	/* 	argv是char **类型,命令行写入是字符串,
		int atoi(const char *nptr) 将字符串转换成int型	*/
	int port = atoi(argv[2]);

	//1.创建套接字
	//int socket(int domain, int type, int protocol);
	int sockfd;
	sockfd = socket(AF_INET,SOCK_STREAM,DEF_VAL);
	if(sockfd == ERR_VAL)
	{
		ERR_LOG("socket");
	}
	printf("====套接字创建成功!\n");

/**  socket地址结构体,成员分别为:ip协议组、端口号以及ip地址  **/
	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family=AF_INET;
	saddr.sin_port = htons(port);
	//将字符串转换为网络字节序
	//int ret = inet_pton(AF_INET,"192.168.6.130",&saddr.sin_addr);	
	int ret = inet_pton(AF_INET,argv[1],&saddr.sin_addr);
	if(ret == ERR_VAL)
	{
		ERR_LOG("inet_pton");
	}
/*------------------socket地址结构体------------------------*/
    
	//2.绑定本机地址与端口号(省略此步,内核可帮做此步骤)
	
	//3.向服务器发起连接
	//int connect(int socket, const struct sockaddr *addr, socklen_t addrlen);
	ret = connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
	if(ret == ERR_VAL)
	{
		ERR_LOG("connect");
	}
	printf("====绑定地址成功!\n");
	
	int n = 0;
	char buf[MAX] = {0};
	//4.数据发送,接受
	while(1)
	{
		//读标准输入数据
		memset(buf,0,sizeof(buf));
		n = read(STDIN_FILENO,buf,sizeof(buf));

		//发送数据
		write(sockfd,buf,n);
		if(!strncmp(buf,"quit",4))
		{
			break;
		}

		//读服务器回发的数据
		memset(buf,0,sizeof(buf));
		n = read(sockfd,buf,sizeof(buf)); 
		if(n <= DEF_VAL)
		{
			printf("客户端退出");
			break;
		}
		printf("n == %d, buf == %s\n",n,buf);
	}

	//关闭套接字,断开连接
	close(sockfd);

	return 0;
}

2.4 字节序

程序在内存中存储的方式

  • 大端字节序(ARM、网络通信数据):网络字节序,低位数据存在高地址位。
  • 小端字节序(Intel芯片X86):本机字节序,低位数据存放在低位地址位。

大小端字节序在内存中存放数据的差异:

在这里插入图片描述

数据转换函数:如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

#include <arpa/inet.h> 
//本机字节序转换为网络字节序:host to network short
uint16_t htons(uint16_t data);

//网络字节序转换为本机字节序:network to host short
uint16_t ntohs(uint16_t data);

//将点分十进制转换为IP网络字节序(二进制数据)
//不能转换255.255.255.255,且只能转换IPV4地址
in_addr_t inet_addr(const char *cp);

//支持IPV6与IPV4,字符串形式转换成网络字节序
int inet_pton(int af,const char *src,void *dst);

//支持IPV6与IPV4,网络字节序转换成字符串
//若成功返回值为指向str的指针,若失败返回指针并置errno
//示例:inet_ntop(AF_INET,&saddr.sin_addr,str,sizrof(str));
const char *inet_ntop(int af,const void *addrptr,char *str,size_t len);

//将IP网络字节序(二进制数据)转换为点分十进制,只支持IPV4
char *inet_ntoa(struct in_addr in);

//将字符串转换为int型
int atoi(const char *nptr);

三、TCP协议重点

netstat -anp:查看网络连接状态指令。

三次握手与四次挥手只有TCP通信才有,UDP没有。

三次握手和四次挥手的过程都是在内核中完成的

服务器的握手操作在accept函数,客户端在connect函数。

3.1 数据包结构

TCP数据报格式:

在这里插入图片描述

端口号:端口号是unsigned short类型,占2个字节,16位。

32位序列号(seq):用来标识从TCP发送端向TCP接收端发送的数据字节流,它表示在这个报文段中的第一个数据字节。

32位确认序列号(ack):包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已完成功收到数据字节序号加1(不是单纯的序号加1,还包括数据字节数)。

首部长度:一般为20字节,实际值是首部长度除以4。

协议标志位

  1. URG:为1时表示紧急指针有效。
  2. ACK:可以理解成一个应答数据包——为1时表示确认序列号有效。
  3. PSH:为1 时表示此包为加急包,应尽快将此包交给应用层。
  4. RST:为1时表示需要重新建立连接。
  5. SYN:可以理解成为握手请求包——为1时表示同步序号用来发起一个连接
  6. FIN:可以理解成挥手请求包——为1时表示发送端完成发送任务。

窗口大小:用于TCP流量控制,指的是缓冲区的大小

​ 发送数据的窗口大小,高速对方在不等待确认的情况下,可以最大发来多大的数据,由于16位最大只能表示65535,如果需要更大的窗口,需要使用选项中的窗口扩大因子选项。

校验和:用于检验数据包传输过程中是否出错。

紧急指针:紧急指针指的是本报文段中紧急数据的最后一个字节的序号,表示前面的数据需要加急处理发送。

选项:其他扩展配置,比如扩大窗口太小,时间戳等等。

3.2 TCP通信特点

点对点的连接

保证数据准确性,内容、时许无误

无丢失、无重复、无差错、按时到达——即高可靠性。

3.3 TCP 出错重传机制

  • TCP向另一端发送数据时,他要求另一端端返回一个确认,如果没收到确认,TCP就自动重传并等待更长的时间,数次失败后才会放弃,这个总时间一般为4~10分钟(TCP协议会动态估算客户与服务器之间的往返时间,在不同的网络环境下超时时间不同)。
  • 注意:TCP不能保证发送的数据一定能被对方接收,这是无法做到的,只是发送失败的时候会中断连接来通知用户,它提供的是数据的可靠递送或故障的可靠通知。

3.4 TCP三次握手

流程

  1. 握手前服务器必须处于listen状态。
  2. 第一个握手包由客户端发送服务器,此包为请求包,发完后客户端处于SYN_SENT状态。
  3. 服务器收到客户端第一个握手包后,发送第二个握手包,此包既为确认包也是请求包,发送完成后服务器处于SYN_RCVD状态。
  4. 客户端收到服务器回包后,确认数据正常后发送第三个握手包,此包为确认包,发送完成后客户端处于EXTABLISHED状态。
  5. 服务器收到最后一个确认包后确认数据无误后状态变更为ESTABLISHED

在这里插入图片描述

标记位解释
SYN请求号标记位
ACK确认号标记位
seq序号,代表请求方将会发送的数据的第一个字节编号
ack返回的确认号,代表接收方收到数据后(也就是seq),代表希望对方下一次传输数据的第一个字节编号
状态位解释
CLOSEDclient处于关闭状态
LISTENserver处于监听状态,等待client连接
SYN_RCVD表示server接收到了SYN报文,当收到client的ACK报文后,它会进入到ESTABLISHED状态
SYN_SENT表示client已发送SYN报文,等待server的第2次握手
ESTABLISTHED表示连接已经建立

第一次握手:客户端第一次发送一条连接请求数据,SYN = 1,ACK = 0就是代表建立连接请求,发送的具体数据第一个字节编号记为x,赋值seq。

第二次握手:服务端收到请求后,返回 客户端的SYN = 1,加上自己的确认号ACK=1,发送的具体数据第一个字节编号记为y,赋值seq,希望客户端下一次返回编号x + 1个字节为止的数据,记为ack = x + 1。

客户端得出客户端发送接收能力正常,服务端发送接收能力也都正常,但是此时服务器并不能确认客户端的接收能力是否正常

第三次握手:客户端收到服务端返回的请求确认后,再次发送数据,原封不动返回ACK = 1,这里就不需要再发送 SYN=1了,为什么呢?因为此时并不是跟服务端进行连接请求,而是连接确认,所以只需要返回ACK = 1代表确认,同样的,发送的具体数据第一个字节编号记为seq = x + 1,希望服务端下次传输的数据第一个字节编号记为ack = y + 1

为什么TCP建立连接时候,要进行3次握手,2次不行吗?
一句话的答案:主要目的:防止server端一直等待,浪费资源。

思考握手为什么需要三次?为什么不是两次或者四次?

  1. 握手的目的在于客户端与服务器双方都能确认双方的收发数据包都正常
  2. 服务器第一个握手包收到后便能确认客户端的发送功能与服务器的接收功能正常。
  3. 客户端收到第二个握手包后,已经能确认出双方收发都没有问题,但此时服务器并不知道自己发送是否正常,客户端能否正常收到数据包。
  4. 服务器收到第三个握手包时才能确认自己的发送功能与客户端接收功能正常。
  5. 三个数据包才能确认双方都能正常发送与接收数据,两个不够,而四个会冗余,降低性能。

3.5 TCP四次挥手

官方称呼为:四分组连接终止序列。后来为了方便理解,有了通俗易懂的四次挥手或四次分手的叫法。

在挥手中,请求断开的可以是服务器也可以是客户端,但往往都是客户端主动断开连接

在这里插入图片描述

状态位: FIN = 1:代表要求释放连接

第一次挥手:client发送一个FIN,用来关闭Client到Server的数据传送,client进入FIN_WAIT_1状态。

第二次挥手:server收到FIN后,发送一个ACK给client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),server进入CLOSE_WAIT状态。

第三次挥手:server发送一个FIN,用来关闭server到client的数据传送,server进入LAST_ACK状态。

第四次挥手:client收到FIN后,client进入TIME_WAIT状态,接着发送一个ACK给server,确认序号为收到序号+1,server进入CLOSED状态,完成四次挥手。

思考

  1. 为什么握手的是时候需要三次,挥手的时候需要四次?

    握手时,服务器收到客户端发来的握手请求后,将自己的握手请求与应答包合并起来发送,而在挥手时,被动方收到FIN报文时,可能不会立即关闭SOCKET,也许还有其他消息没有处理完,故先回复一个ACK应答包,等待自己所有数据处理完后才会发送自己的FIN包

  2. 为什么主动方最后还需要经历TIME_WAIT状态才会关闭?

    原因一:TIME_WAIT状态需要经过2MSL(两倍的最大报文段生存时间)才会回到CLOSE状态,由于网络本身不可靠,挥手最后一个ACK应答包可能会丢失,假设这个包丢失后,被动方会再次发送一个FIN挥手请求包,此时如果主动方已经关闭连接,则被动方会认为连接出错

    原因二:在相同的两个端可能在断开连接后紧接者再次握手连接,这时上次通信中的迷途数据包到达此端口则会认为是这次的数据包,等待2MSL也可以让网络中所有的迷途数据包消亡,则不会影响后面的通信

四、UDP通信

4.1 UDP通信概念

  • UDP是一个简单的传输层协议,应用进程往一个UDP套接字中写入一个消息,然后被封装成一个数据报,进而被封装成一个IP数据报,然后发送到目的地。
  • UDP不保证数据报到达目的地,不保证数据报先后顺序跨网络后保持不变,不保证数据报只到达一次。
  • 每个UDP数据报都有一个长度,如果一个数据报正确的到达目的地,那么该数据报的长度会随数据一道传递给接收端。

4.2 与TCP的区别

  • 相同点:都是传输层协议,通过端口号标识一个进程
  • 不同点
    1. UDP无连接,没有出错重传,流量控制,三次握手等机制,传输数据不可靠,数据有时会有丢失和失序,但传输速率快
    2. TCP没有任何记录边界,而UDP会记录数据报长度
    3. 应用场景:流媒体软件、直播、QQ聊天等。

4.3 UDP通信流程

服务器(server)

//1.创建套接字----socket()----类型选择SOCK_DGRAM,为数据报套接字。
//2.绑定IP地址与端口号----bind()
//3.数据发送接收----sendto()/recvfrom()
//4.关闭套接字

数据的发送:sendto()

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
  • 返回值:成功发送返回发送数据的字节数,发送失败返回-1

    注意:此处发送失败单指自己有没有发送出去,不考虑对方是否接收到

  • socket:套接字。

  • buf:发送数据的首地址。

  • len:要发送数据的长度。

  • flags:加0时表示阻塞方式。

  • dest_addr:目标的地址结构的首地址。

  • addrlen:地址结构的长度。

数据的接受:recvfrom()

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
  • 返回值:成功返回接收到数据的字节数,失败返回-1。
  • sockfd:套接字。
  • buf:接收数据的首地址。
  • len:接收数据缓冲区大小。
  • src_addr:源的地址结构的首地址(可选)。
  • addrlen:地址结构长度(可选)。

客户端(client)

//1.创建套接字----socket()
//2.数据发送接收----sendto()/recvfrom()
//3.关闭套接字----close()

4.4 UDP的connect函数

UDP的connect函数

4.5 UDP并发服务器设计

UDP并发服务器设计

五、IO多路复用

目的与功能解决阻塞IO影响其他程序执行和非阻塞IO造成的资源浪费的情况。

IO多路复用可以同时监听多个IO操作。

5.1 5种I/O模型

  1. 阻塞式I/O:是最流行的I/O模型,默认的情况下所有套接字都是阻塞的。
  2. 非阻塞式I/O:设置成非阻塞是通知进程,当所请求的I/O操作非得把本进程置于休眠状态才能完成时,则不要置于休眠状态。
  3. I/O复用: 同时阻塞监听多个I/O操作,任意一个或多个出发后都会继续执行。
  4. 信号驱动式I/O:内核在描述符就绪时会发送SIGIO信号通知。
  5. 异步I/O:给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告知内核再操作完成后如何通知我们。

5.2 同步I/O与异步I/O

  1. 同步IO操作:导致请求阻塞进程,直到I/O操作完成。

  2. 异步I/O操作:不导致请求阻塞进程。

    [5.1](#5.1 5中I/O模型)中的五种模型中,前四种都为同步I/O,因为其中真正的I/O操作都是阻塞的。

在这里插入图片描述

5.3 阻塞I/O与非阻塞I/O

5.3.1 阻塞I/O

函数fgets()read()write()recvfrom()...

读阻塞:当运行执行到对应函数时,如果读取数据时没有对应资源,程序会在函数处阻塞,直到有读取的资源为止,程序才会继续运行。

写阻塞:当写入缓冲区空间不足时,就会造成写阻塞,直到有足够的空间,写操作继续执行。例外:sendto(),此函数不会阻塞。

缺陷:可能会干扰其他程序执行。

5.3.2 非阻塞I/O

  • 当程序执行到非阻塞IO操作时,不会在原地阻塞,而是直接返回错误结果。

  • 这是可以调用fcntl()函数,将阻塞函数设置成非阻塞形式。

  • 缺陷:设置成非阻塞后,内核会循环判断有没有相关资源,如果没有会报错,有则执行,浪费内核空间。

    int fcntl(int fd,int cmd,.../* arg */);
    //@ret :当cmd填写为F_GETFL时获取文件描述符当前属性
    /*@param:	fd:要设置非阻塞IO的文件描述符
    		cmd:F_GETFL,获取对应IO属性。F_SETFL,设置位对应属性
    		arg:只有当cmd填F_SETFL时,才需为其设置属性。
    */
    
    //eg:将标准输入设置为非阻塞
    int flag = -1;
    flag = fcntl(0,F_GETFL,0);
    flag |= O_NONBLOCK;
    fcntl(0,F_SETFL,flag);
    

5.4 select

5.4.1 原理

  1. 将要进行IO的文件描述符添加到文件描述符集合(位图)。
  2. 将这个文件描述符集合拷贝到内核中。
  3. 内核需要遍历文件描述符集合,循环检测对应的文件描述符是否可以进行IO操作(可读 or 可写)。

5.4.2 select函数

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

参数

  • nfds:最大的文件描述符+1(文件描述符集合)。
  • readfds:文件描述符读表。(一般只用这一个)。
  • writefds:文件描述符写表。(这个一般不用)。
  • exceptfds:文件描述符异常处理表。(这个一般不用)
  • timeout:超时时间——此参数有三个可能性:
    1. 永远等待下去,仅在一个描述符准备好I/O时才返回,此时设置为空指针。
    2. 等待固定时间,在有一个描述符准备好时返回,但不超过由该结构体设置的时间
    3. 根本不等待,为轮询,此时结构体内填0。
  • 返回值:成功返回监听文件描述符个数,失败返回-1,0表示监听超时。

操作文件描述符表函数

void FD_CLR(int fd, fd_set *set);	//将文件描述符表的fd位清零
int FD_ISSET(int fd, fd_set *set);	//判断文件描述符表里的fd是否置1,是返回1,不是返回0
void FD_SET(int fd, fd_set *set); 	//将指定的文件描述符集合里的fd位置1
void FD_ZERO(fd_set *set);		  //将指定的文件描述符集合里所有位清零

使用流程

  1. 定义一张文件描述符,进行初始化。
  2. 通过select函数检测文件描述符。
  3. 记录需要检测的表,轮询更新表并处理需要操作的文件描述符。

缺点

  1. 内核使用轮询方式检测表中描述符是否就绪,文件描述符越多,消耗资源越多。
  2. 文件描述符集合使用表最大为1024个文件描述符。
  3. 每次文件描述符集合更新时,重新拷贝到内核中。
  4. 结果表会覆盖原始表,select函数实现中,一旦发现就绪的文件描述符就会返回,此时表中未就绪的描述符会被清除,每次都需要重新向新表加入需要检测的文件描述符。

5.4.3 描述符就绪的条件

准备好读的条件

  • 接收缓冲区数据大于等于接收缓冲区低水位标记的当前大小(默认为1),读操作将返回一个大于0的值。
  • 该连接的读半部关闭,也就是接收到了FIN包,读操作将返回0。
  • 该套接字是一个监听套接字,且已完成的连接数不为0。
  • 有一个套接字的错误待处理,读操作将返回-1。

准备好些的条件

  • 发送缓冲区中可用空间字节大于等于发送缓冲区低水位标记的当前大小。TCP与UDP而言,默认值为2048。
  • 该连接的写半部已关闭。
  • 使用非阻塞的connect的套接字已连接,或者connect失败。
  • 有一个套接字错误待处理,写操作返回-1。

异常的条件

  • 一个套接字存在带外数据或仍处于带外标记。

5.4.4 实例

demo:服务器端。

#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <sys/select.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define ERR_VAL -1
#define DEF_VAL 0
#define ERR_LOG(VAL) do{perror(VAL);exit(EXIT_FAILURE);}while(0)

int main(int argc, const char *argv[])
{
	//1.创建套接字
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd == ERR_VAL) {
		ERR_LOG("socket");
	}
	printf("====创建套接字成功!\n");
	
	//2.绑定端口
	struct sockaddr_in saddr,caddr;
	memset(&saddr,0,sizeof(saddr));
	memset(&caddr,0,sizeof(caddr));
	saddr.sin_family = AF_INET;
	//saddr.sin_addr.s_addr = htonl(INADDR_ANY);
	saddr.sin_port = htons(8888);
	inet_pton(AF_INET,"192.168.6.130",&saddr.sin_addr);
	int ret = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
	if(ERR_VAL == ret) {
		ERR_LOG("bind");
	}
	printf("====绑定成功!\n");
	
	//3.监听listen
	ret = listen(sockfd,5);
	if(ERR_VAL == ret) {
		ERR_LOG("listen");
	}
	printf("====监听成功!\n");

	//4.创建位图并清空
	fd_set fds , tmpfds;//要监控的文件描述符集
	FD_ZERO(&fds);
	FD_ZERO(&tmpfds);
	FD_SET(sockfd,&fds);//将sockfd加入到fds读集合中
	tmpfds = fds;
	int nfds = sockfd + 1;

	socklen_t clen = sizeof(caddr);
	struct timeval t = {5,0};


	char buf[1024] = {0};
	int connfd;

	while(1) {
		//更新位图
		fds = tmpfds;
		t.tv_sec = 5;
		t.tv_usec = 0;
		int retval = select(nfds,&fds,NULL,NULL,&t);
		if (ERR_VAL == retval) {
			ERR_LOG("select");
		}
		else if(DEF_VAL == retval){
			printf("超时!\n");
			continue;
		}
	
		int i = 0;
		for(i = 3;i < nfds; i++) {
			if(!FD_ISSET(i,&fds)) {
				continue;
			}
			if(i == sockfd) {
				//等待客户端连接--accept()
				connfd = accept(sockfd,(struct sockaddr *)&caddr,&clen);
				if(ERR_VAL == connfd) {
					ERR_LOG("accept!");
				}
				char str[20] = {0};
				inet_ntop(AF_INET,&caddr.sin_addr,str,sizeof(str));
				printf("客户端[ip:%s-port:%d]连接成功!\n",str,caddr.sin_port);
				FD_SET(connfd,&tmpfds);
				if(connfd + 1 > nfds) {
					nfds = connfd + 1;
				}
			}
			else {
				//读数据
				memset(buf,0,sizeof(buf));
				int len = read(i,buf,sizeof(buf));
				if(len <= DEF_VAL) {
					printf("客户端退出!\n");
					close(i);
					FD_CLR(i,&tmpfds);
					continue;
				}
				printf("read %d bytes data:%s",len,buf);
		
				//写数据
				write(i,buf,len);
			}
		}
	}

	close(sockfd);
	return 0;
}

5.5 poll机制

5.5.1 特点

  1. poll监听的文件描述符没有1024的限制(可以通过修改配置文件来选择监听上限)
    • 可通过ulimit -a查看当前设置的上限(open file)。
    • 在此文件配置/etc/security/limits.conf
  2. 文件描述符初始表和返回表是分离的。
  3. 会循环检测对应文件描述符是否有操作,比较消耗资源。

5.5.2 poll函数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

类型struct pollfd

struct pollfd {
       int fd;         /* file descriptor */
       short events;     /* requested events */
       short revents;    /* returned events */
     };

fd:需要检测的文件描述符。

events:请求检测的事件。

revents:监控事件中满足条件返回的事件。

参数

fds:需要监听的所有文件描述符结构体的数组。

nfds:监控数组中有多少文件描述符需要被监控。

timeout:超时事件,单位为毫秒,-1为无超时。

  • -1: 阻塞等,#define INFTIM -1 Linux中没有定义此宏

  • 0: 立即返回,不阻塞进程

  • >0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值

返回值:成功返回就绪文件描述符个数,返回0超时,返回-1出错。

如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0。

相较于select而言,poll的优势:

​ 1. 传入、传出事件分离。无需每次调用时,重新设定监听事件。

​ 2. 文件描述符上限,可突破1024限制。能监控的最大上限数可使用配置文件调整。

5.5.3 实例

demo:客户端

#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define ERR_VAL -1
#define DEF_VAL 0
#define ERR_LOG(VAL) do{   			   \
					perror(VAL); 	   \
					exit(EXIT_FAILURE);\
					}while(0)

int main(int argc, const char *argv[])
{
	//1.创建套接字
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(ERR_VAL ==  sockfd) {
		ERR_LOG("socket");
	}
	printf("====创建套接字成功!\n");

	//客户端不需要绑定ip信息
	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family = AF_INET;
	//saddr.sin_addr.s_addr = htonl(INADDR_ANY);
	saddr.sin_port = htons(8888);
	inet_pton(AF_INET,"192.168.6.130",&saddr.sin_addr);

	//conect连接
	int ret = connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
	if(ERR_VAL == ret) {
		ERR_LOG("connect");
	}
	printf("====连接成功!\n");

	fd_set fds,tmpfds;
	FD_ZERO(&fds);
	FD_ZERO(&tmpfds);
	FD_SET(sockfd,&fds);
	FD_SET(0,&fds);

	int nfds = sockfd + 1;
	tmpfds = fds;
	char buf[128] = {0};
	int n = 0;
	while(1) {
		fds = tmpfds;
		ret = select(nfds,&fds,NULL,NULL,NULL);
		if(ERR_VAL == ret) {
			ERR_LOG("select!");
		}

		if(FD_ISSET(0,&fds)) {
			memset(buf,0,sizeof(buf));
			fgets(buf,sizeof(buf),stdin);
			n = write(sockfd,buf,sizeof(buf));
			if(n-1 <= DEF_VAL) {
				printf("客户端退出!\n");
				break;
			}
		}

		if(FD_ISSET(sockfd, &fds)) {
			memset(buf,0,sizeof(buf));
			n = read(sockfd,buf,sizeof(buf));
			if(n <= DEF_VAL) {
				printf("服务端退出!\n");
				break;
			}
			printf("read %d bytes data :%s",n,buf);
		}
	}

	return 0;
}

服务器:

#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <sys/select.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <poll.h>

#define ERR_VAL -1
#define DEF_VAL 0
#define ERR_LOG(VAL) do{perror(VAL);exit(EXIT_FAILURE);}while(0)

int main(int argc, const char *argv[])
{
	//1.创建套接字
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd == ERR_VAL) {
		ERR_LOG("socket");
	}
	printf("====创建套接字成功!\n");
	
	//2.绑定端口
	struct sockaddr_in saddr,caddr;
	memset(&saddr,0,sizeof(saddr));
	memset(&caddr,0,sizeof(caddr));
	socklen_t clen = sizeof(caddr);
	saddr.sin_family = AF_INET;
	//saddr.sin_addr.s_addr = htonl(INADDR_ANY);
	saddr.sin_port = htons(8888);
	inet_pton(AF_INET,"192.168.6.130",&saddr.sin_addr);
	int ret = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
	if(ERR_VAL == ret) {
		close(sockfd);
		fprintf(stderr,"fail to bind,the socket is reclaimed!\n");
		exit(EXIT_FAILURE);
	}
	printf("====绑定成功!\n");
	
	//3.监听listen
	ret = listen(sockfd,5);
	if(ERR_VAL == ret) {
		ERR_LOG("listen");
	}
	printf("====监听成功!\n");

    //poll函数的使用
    struct pollfd client[100];
    for(int i = 0;i < 100;i++)
    {
        client[i].fd = -1;
    }
	
   	//监控读事件--sockfd添加进读数组
   	client[0].fd = sockfd;
   	client[0].events =  POLLIN;

   int nfds = 1;   //读数组最大元素个数

	char buf[1024] = {0};
	int connfd;

	while(1) {
        ret = poll(client,nfds,-1);
		if (ERR_VAL == ret) {
			ERR_LOG("select");
		}
		else if(DEF_VAL == ret){
			printf("超时!\n");
			continue;
		}
	
		int i = 0;
		for(i = 0;i < nfds; i++) {
			if(!(client[i].revents & POLLIN)) {
				continue;
			}

			if(client[i].fd == sockfd) {
				//等待客户端连接--accept()
				connfd = accept(sockfd,(struct sockaddr *)&caddr,&clen);
				if(ERR_VAL == connfd) {
					ERR_LOG("accept!");
				}
				char str[20] = {0};
				inet_ntop(AF_INET,&caddr.sin_addr,str,sizeof(str));
				printf("客户端[ip:%s-port:%d]连接成功!\n",str,caddr.sin_port);
				
				//更新事件,自增nfds
                client[nfds].fd = connfd;
                client[nfds].events = POLLIN;
				if(nfds < 100) {
                    nfds++;
                }
			}
			else {
				//读数据
				memset(buf,0,sizeof(buf));
				int len = read(client[i].fd,buf,sizeof(buf));
				if(len <= DEF_VAL) {
					printf("客户端退出!\n");
					close(client[i].fd);

                    client[i].fd = client[nfds].fd;
                    client[nfds].fd = -1;
					if(--nfds == 0) {
						break;
					}
					continue; 
				}
				printf("read %d bytes data:%s",len,buf);
		
				//写数据
				write(client[i].fd,buf,len);
			}
		}
	}

	close(sockfd);

	return 0;
}

5.6 epoll机制

5.6.1 特点

epoll是Linux(只能在linux中使用)下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

目前epoll是linux大规模并发网络程序中的热门首选模型。

  1. epoll只能在linux中使用。

  2. 在epoll机制的基础上不需要一直循环检测文件描述符

    • 可通过平衡二叉树的方式查找文件描述符。
  3. 缺点占用资源多,树形结构,链表,内存映射若监控2,3个太浪费了。

5.6.2 基础API

  1. 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。

    #include <sys/epoll.h>
    int epoll_creat(int size)
        size:监听数目(内核参考值)
        返回值:成功,返回非负文件描述符;失败返回-1,并设置相应的errno
    

    注意:size参数只是一个对内核的建议,现在已经被忽略了,所以这个参数有些多余,现在常用epoll_creat1函数(注意后面是数字1,不是字母l)

    #include <sys/epoll.h>
    int epoll_creat1(int flags)
        flags:只需填写EPOLL_CLOEXEC,其值为0,表示进程被替换时关闭文件描述符。
        返回值:成功返回操作树的句柄,失败返回-1.
    
  2. 控制某个epoll监控的文件描述符上的事件:注册、修改、删除。

    #include <sys/epoll.h>
    int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
        epfd:为epoll_creat的句柄
        op:	对应文件描述符的操作,用3个宏本来表示
            EPOLL_CTL_ADD(注册新的fd到epfd)EPOLL_CTL_MOD(修改已经注册的fd的监听事件)EPOLL_CTL_DEL(从epfd删除一个fd);
        event:告诉内核需要监听的事件。
        struct epoll_event {
            __uint32_t events; /* Epoll events */
            epoll_data_t data; /* User data variable */
        };
        typedef union epoll_data {
            void *ptr;
            int fd;
            uint32_t u32;
            uint64_t u64;
        } epoll_data_t;
    
        EPOLLIN :	表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
        EPOLLOUT:	表示对应的文件描述符可以写
        EPOLLPRI:	表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
        EPOLLERR:	表示对应的文件描述符发生错误
        EPOLLHUP:	表示对应的文件描述符被挂断;
        EPOLLET: 	将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
        EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
            
        返回值:成功返回0;失败返回-1,并设置相应的errno
    
  3. 等待所监控文件描述符上有事件的产生,类似于select()调用

    #include <sys/rpoll.h>
    int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)
    	events:用来存内核得到事件的集合,可简单看作数组。
    	maxevents:告诉内核这个events有多大,events数组的长度。
    	timeout:是超时时间。
        	-1:阻塞	0:立即返回,非阻塞	>0:指定毫秒
       返回值:成功返回就绪的文件描述符个数,时间到了返回0,出错返回-1
    

5.6.3 实例

#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/epoll.h>

#define DEF_VAL 0
#define ERR_VAL -1
#define ERR_LOG(VAL) do{perror(VAL); exit(EXIT_FAILURE);}while(0)

typedef struct sockaddr SA;

int main(int argc, const char *argv[])
{
	if (argc != 2) {
		printf("输入参数错误![exe] [port]\n");
		return 1;
	}

	int sockfd = socket(AF_INET, SOCK_STREAM, DEF_VAL);   //创建一个流式套接字
	if (sockfd == ERR_VAL) {
		ERR_LOG("socket");
	}

	printf("===socket success! sockfd = %d\n", sockfd);
	
	struct sockaddr_in saddr, caddr;
	socklen_t caddr_len = sizeof(caddr);
	memset(&caddr, 0, caddr_len);
	memset(&saddr, 0, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(atoi(argv[1]));
	saddr.sin_addr.s_addr = INADDR_ANY;  //"0.0.0.0"
	/*
	int ret = inet_pton(AF_INET, "192.168.2.135", &saddr.sin_addr);
	if (ret == ERR_VAL) {
		ERR_LOG("inet_pton");
	}
	*/

	int ret = bind(sockfd, (SA *)&saddr, sizeof(saddr));
	if (ret == ERR_VAL) {
		ERR_LOG("bind");
	}

	printf("===bind success!\n");

	ret = listen(sockfd, 5);  //非阻塞
	if (ret == ERR_VAL) {
		ERR_LOG("listen");
	}
	printf("===listen success!\n");

	int treefd = epoll_create1(0);

	struct epoll_event ev1, res[10];
	ev1.data.fd = sockfd;
	ev1.events = EPOLLIN;

	epoll_ctl(treefd, EPOLL_CTL_ADD, sockfd, &ev1);

	while (1) {
		ret = epoll_wait(treefd, res, 10, 5000);
		if (ret == ERR_VAL) {
			ERR_LOG("epoll");
		}
		else if (ret == 0) {
			printf("定时器到点\n");
			continue;
		}

		for (int i = 0; i < ret; ++i) {

			if (res[i].data.fd == sockfd) {
				int connfd = accept(sockfd, (SA *)&caddr, &caddr_len);  //阻塞
				if (connfd == ERR_VAL) {
					ERR_LOG("accept");
				}
				char caddr_buf[20] = {0};
				printf("===accept success! addr: %s:%d\n", 
						inet_ntop(AF_INET, &caddr.sin_addr, caddr_buf, sizeof(caddr_buf)),
						ntohs(caddr.sin_port));
				struct epoll_event ev2;
				ev2.data.fd = connfd;
				ev2.events = EPOLLIN;
				epoll_ctl(treefd, EPOLL_CTL_ADD, connfd, &ev2);
			}
			else {
				char buf[1024] = {0};
				int len = read(res[i].data.fd, buf, sizeof(buf));
				if (len <= 0) {
					printf("客户端异常断开!\n");
					close(res[i].data.fd);
					epoll_ctl(treefd, EPOLL_CTL_DEL, res[i].data.fd, &(res[i]));
					continue;
				}
				printf("客户端发来数据:%s", buf);
				write(res[i].data.fd, buf, len);

			}
		}
	}
	close(sockfd);

	return 0;
}

5.7 总结

  1. 主要操作用于单进程。
  2. 实时性不好。
  3. 一般只用于短作业。
  4. 具体选择那种机制根据特点自行选择。

六、SQlite3 使用

6.1 SQlite3 命令

在线安装

sudo apt-get install sqlite3
sudo apt-get install sqlite3 libsqlite3-dev

**数据文件的打开:**终端输入sqlite3 <数据库名>.db ,若存在,直接打开。不存在,创建表之后,创建数据库文件。

常见命令:(系统命令前面加点)

.help:显示帮助信息。

.quit:退出SQLITE3。

.exit:退出

.database:显示当前打开的数据库文件。

.tables:显示数据库中所有的表名。

schema:查看表的结构。

SQL语句:与数据库进行交互。(都以;结尾)

  1. 创建数据表:(不分大小写)

    create table table_name ( //table_name : 数据表名
        column1 datatype,	//columnN : 表示第N个字段名
        column1 datatype,
        ...
        columnN datatype,
    	);
    //datatype :字段名类型
    /* 
    	NULL : NULL值
    	INTEGER : 值是一个带符号的整数,根据值的大小存储在1、2、3、4、6或8字节中。
    	REAL : 值是一个浮点值,存储为8个字节的IEEE浮点数字
    	TEXT : 值是一个字符串,使用数据库编码(UTF-8,UTF-16BE或UTF-16LE)存储
    	BLOB : 值是一个blob数据(二进制数据),完全根据他的输入存储。
    */
    
    //创建一个表
    create table stu(id int,name text,age int,sorce real);
    //设置主键--即主键值不能重复
    create table stu(id int primary key,name text,sorce real);
    //存在则不创建
    create table if not exists stu(id int,name text,sorce real); 
    
  2. 插入一条数据:

    格式1 : insert into stu[(id,name,score)]values(1001,'hehe',99);
    格式2 : insert into stu values(1001,'hehe',99);
    
  3. 查找数据库记录:

    //1.显示列的名字 :.headers on
    //2.按照列显示:.mode column
    
    select * from stu;
    select * from stu where score = 80;
    select * from stu where score = 80 and name = 'zhangsan';
    select * from stu where score = 80 or name = "zhangsan";
    select name,score from stu; //查询指定的字段
    select * from stu where score >= 85 and score < 90;
    
  4. 更新记录:

    updata stu set age = 20 where id = 1003;
    updata stu set age = 30,score = 82 where id = 1003;
    
  5. 删除一条记录:

    delete from stu where id = 1003 and name = "zhangsan";
    
  6. 删除数据库表:

    drop table stu;
    

6.2 Sqlite3 API编程

概念:SQLITE3数据库针对不同的语言提供了相应的编程接口,下面介绍的是关于c语言接口。

6.2.1 打开数据库文件

int sqlite3_open(char *path,sqlite3 **ppDb);

功能:打开slite3 数据库
path:数据库文件路径
db:指向数据库句柄的指针
返回值:成功返回SQLITE_OK,失败返回错误码(非零值)

6.2.2 执行sql语句:

int sqlite3_exec(
	  sqlite3* ppDb,                             
      /* An open database */
	  const char *sql,                          
      /* SQL to be evaluated */
	  int (*callback)(void* arg,int,char**,char**),
      /* Callback function */
	  void * arg,       
      /* 1st argument to callback */
	  char **errmsg      
      /* Error msg written here */
	);

**函数功能:**执行sql指定的数据库命令操作。
参数说明:
ppDb:splite3_open操作时的第二个参数dB数据操作句柄
sql:SQL命令,可以有多条命令组成。
callback:执行完该函数的回调函数,只有sql为查询语句的时候才会执行此语句。
void * arg: 作为callback回调函数的第一个参数传入。
errmsg:获取函数错误是的错误码。

  • 回调函数

    int sqlite_callback(
      void*      para, 
      int         f_num, 
      char**    f_value, 
      char**    f_name
    ); 
    

    说明:每找到一条记录自动执行一次回调函数
    para:传递给回调函数的参数
    f_num:记录中包含的字段数目
    f_value:包含每个字段值的指针数组
    f_name:包含每个字段名称的指针数组
    返回值:成功返回0,失败返回-1

6.2.3 查询表数据:

int sqlite3_get_table(
	  sqlite3 *ppDb,          
      /* An open database */
	  const char *zSql,     
      /* SQL to be evaluated */
	  char ***dbResult,     
      /* Results of the query */
	  int *pnRow,           
      /* Number of result rows written here */
	  int *pnColumn,       
      /* Number of result columns written here */
	  char **pzErrmsg       
      /* Error msg written here */
	);

函数功能:获取数据库表格
函数参数
ppDb:同上
zSql:SQL命令
dbResult:查询结果,它依然是一维数组,它内存布局是:字段名称,后面是紧接着是每个字段的值。
pnRow:查出具体有多少行,即多少条记录(不包括字段名那行)。
pnColumn:查出具体有多少列,即多少字段
pzErrmsg:错误信息

6.2.4 获取错误码:

const char *sqlite3_errmsg(sqlite3*ppDb);

6.2.5 关闭数据库:

int sqlite3_close(sqlite3 * ppDb);

6.2.6 释放结果表:

void sqlite3_free_table(char **result);

例子:

注意,编译的时候加-lsqlite3

#include <stdio.h>
#include <sqlite3.h>

int main(int argc, char *argv[])
{
    sqlite3 *db;
    char *errmsg;
    if( sqlite3_open("./student.db",&db) != SQLITE_OK ) {
        printf("%s\n",sqlite3_errmsg(db));
        return -1;
    } 
    else  {
        printf("open student.db success\n");
    }
    
    if(sqlite3_exec(db,"create table stu(name char);",NULL,NULL,&errmsg) != SQLITE_OK) {
        printf("%s\n",errmsg);
    }
    printf("table success\n");
    sqlite3_close(db);
    return 0;
}

七、套接字属性

套接字可以设置多种特性:窗口大小、最大序列号、TTL等信息。套接字设置是分层的,可以通过参数设置通用套接字选项IP选项TCP选项,和套接字的相关选项。每一层的可设置参数如下:

在这里插入图片描述

7.1 设置套接字属性:

int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);

sock :将要被设置或获取选项的套接字
level :选项所在的协议层
optname :需要访问的选项名称。
optval :指向包含新选项值的缓冲。该参数需要与optname配合,创建一个对应类型的变量,传入其地址如果是int型,不能填0,否则设置不生效
optlen :现选项的长度。 该参数为第四个参数的长度

返回值:成功执行时返回0,失败返回-1,并置错误码errno

错误码解释

EBADF:sock不是有效的文件描述符。

EFAULT:optval指向的内存并非有效的进程空间。

EINVAL:在调用setsockopt()时,optlen无效。

ENOPROTOOPT:指定的协议层不能识别选项。

ENOTSOCK:sock描述的不是套接字。

EAGIN or EWOULDBLOCK:套接字被设置为非阻塞状态。

EINTR:如果进程在一个慢系统调用(slow system call)中阻塞时,当捕获到某个信号且相应信号处理函数返回时,这个系统调用中被中断,调用返回错误,设置errno为EINTR(相应的错误描述为“interrupted system call”)。

简单案例

允许重用本地地址和端口:(socket函数之后 - bind函数之前)

int val = 1;
if(setsockopt(sockfd , SOL_SOCK, SO_REUSEADDR, &val, sizeof(val) < 0) {
    perror("setsockopt!");
    exit(EXIT_FAILURE);
}

设置套接字接收超时:

struct timeval tm = {5,0};
if(setsockopt(connfd, SOL_SOCKET, SO_RCVTIMEO, &tm ,sizeof(tm) < 0) {
    perror("set time");
    exit(EXIT_FAILURE);
}
  • 超时read会设置errno

    char buf[1024] = {0}:
    int len = read(connfd ,buf ,1024);
    if(len < 0) {
        if(errno == EAGIN) {
            printf("超时退出\n");
            close(connfd);
            break;
        }
    }
    
  • 其他两种超时检测方式:

    1. 通过信号定时器完成。
    2. 通过select、poll、epoll完成。:不可在此基础上设置套接字属性增加超时功能

7.2 读取套接字属性

int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);

用法:几乎与setsockopt一样,optval为出参,获取属性,长度需要传入地址。

7.3 广播与组播(多播)

在UDP协议下使用广播与组播。

7.3.1 广播

在局域网范围内,发送方通过广播向所有接收方发送信息。

注意:在广播代码中抛弃客户端与服务器的观念,客户端与服务器是主观的,不是客观规定的

套接字创建之后默认不允许使用广播

应用场景

  1. 局域网发送文件
  2. VNC
  3. 电脑自动分配IP的过程

缺点

广播方式发送给所有的主机,过多的广播会大量占用网络宽带,造成广播风暴,影响正常的通信。

广播地址必须是主机号全为1

  • A类地址:

    一般大型公司或美国高效使用。

    第一个字节为网络号,后三个字节为主机号,以0开头。

    范围:1.0.0.0~126.255.255.255 要注意:127地址一般不能用,为本地环回地址。

    A类的网络号为126个,主机号有2^24-2个,全0与全1不能用。

  • B类地址:

    个人PC可分配IP。

    前两个字节为网络号,后两个字节为主机号,以10开头。

    范围:128.0.0.0~191.255.255.255。

    B类网络号为64个,主机号有2^16-2个。

  • C类地址:

    前三个字节为网络号,最后一个字节为主机号,以110开头。

    范围:192.0.0.0~233.255.255.255。

  • D类地址:

    主要用于UDP组播,少见

    部分网络号和主机号,前四位固定为1110

    范围:224.0.0.1~239.255.255.255

代码流程

广播发送端

//1.创建数据报套接字
//2.设置套接字属性,允许发送广播
//3.设置广播地址
//4.通过sendto函数发送数据
//5.关闭套接字
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#define ERRLOG(VAL) do{perror(VAL);exit(EXIT_FAILURE);}while(0)

int main(int argc,const char **argv)
{
    struct sockaddr_in serveraddr;
    socklen_t slen = sizeof(serveraddr);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0) {
        ERRLOG("fail to socket!");
    }

    int val = 1;
    if( setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&val,sizeof(val)) < 0) {
        ERRLOG("fail to setsockopt!");
    }

    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(8888);
    if (inet_pton(AF_INET, "192.168.6.255", &serveraddr.sin_addr) <= 0) {
        perror("inet_pton");
        exit(EXIT_FAILURE);
    } 

    while(1) 
    {
        char buf[1024] = {0};
        fgets(buf,sizeof(buf),stdin);
        buf[strlen(buf)-1] = '\0';
        sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&serveraddr,slen);
    }

    close(sockfd);
    return 0;
}

广播接收端

//1.创建数据报套接字
//2.绑定:IP必须写INADDR_ANY或写广播IP地址
//3.通过recvfrom接收数据
//4.关闭套接字
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#define ERRLOG(VAL) do{perror(VAL);exit(EXIT_FAILURE);}while(0)

int main(int argc,const char **argv)
{
    struct sockaddr_in serveraddr,clientaddr;
    socklen_t clen = sizeof(clientaddr);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0) {
        ERRLOG("fail to socket");
    }

    bzero(&clientaddr,sizeof(clientaddr));
    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    serveraddr.sin_port = htons(8888);
    //if (inet_pton(AF_INET, "192.168.6.255", &serveraddr.sin_addr) <= 0) {
     //   perror("inet_pton");
      //  exit(EXIT_FAILURE);
   // }   

    //int val = 1;
    //setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&val,sizeof(val));

    if(bind(sockfd,(const struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        ERRLOG("fail to bind");
    }

    while(1) 
    {
        char buf[1024] = {0};
        recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&clientaddr,&clen);
        printf("%s\n",buf);
    }

    close(sockfd);
    return 0;
}

7.3.2 组播

组播特点

  1. 组播(又称为多播)是一种折中的方式,只有加入某个多播组的主机才能接收到数据
  2. 组播方式既可以发给多个主机,又要避免像广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
  3. 一台机器发送组播包,所有加入这个组播的主机都会收到。
  4. 组播地址,D类IP:224.0.0.0 ~ 239.255.255.255

代码流程

发送组播包:

//1.创建数据报套接字
//2.填充组播地址
//3.通过sendto发送组播包
//4.关闭套接字
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#define ERRLOG(VAL) do{perror(VAL);exit(EXIT_FAILURE);}while(0)

int main(int argc,const char **argv)
{
    struct sockaddr_in serveraddr;
    socklen_t slen = sizeof(serveraddr);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0) {
        ERRLOG("fail to socket!");
    }

    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(8888);
    if (inet_pton(AF_INET, "233.233.233.233", &serveraddr.sin_addr) <= 0) {
        perror("inet_pton");
        exit(EXIT_FAILURE);
    } 


    while(1) 
    {
        char buf[1024] = {0};
        fgets(buf,sizeof(buf),stdin);
        buf[strlen(buf)-1] = '\0';
        sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&serveraddr,slen);
    }

    close(sockfd);
    return 0;
}

接收组播包:

//1.创建数据报套接字
//2.加入组播包
//3.填充组播地址
//4.绑定组播地址
//5.通过recvfrom接收组播包

步骤二结构体:

struct ip_mreq {

​ struct in_addr imr_multiaddr; //为组播地址

​ struct in_addr imr_interface; //为本机ip

}

struct in_addr {

​ uint32_t s_addr;

}

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#define ERRLOG(VAL) do{perror(VAL);exit(EXIT_FAILURE);}while(0)

int main(int argc,const char **argv)
{
    struct sockaddr_in serveraddr,clientaddr;
    socklen_t clen = sizeof(clientaddr);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0) {
        ERRLOG("fail to socket");
    }

    struct ip_mreq multica;
    multica.imr_multiaddr.s_addr = inet_addr("233.233.233.233");
    multica.imr_interface.s_addr = inet_addr("192.168.6.130");
    setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&multica,sizeof(multica));


    bzero(&clientaddr,sizeof(clientaddr));
    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    serveraddr.sin_port = htons(8888); 

    if(bind(sockfd,(const struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        ERRLOG("fail to bind");
    }

    while(1) 
    {
        char buf[1024] = {0};
        recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&clientaddr,&clen);
        printf("%s\n",buf);
    }

    close(sockfd);
    return 0;
}

八、UNIX域套接字

特点

  1. socket同样可用于本地通信。
  2. 创建套接字时使用本地协议AF_UNIX或者AF_LOCAL。
  3. 和其他进程间通信方式相比使用简单,效率更高。
  4. 常用于前后台进程通信。
  5. 同样也分为流式套接字和用户数据报套接字

地址结构

//本地地址结构
struct sockaddr_un {
    sa_family_t sum_family;
    char sun_path[108];//套接字文件的路径。
}
//填充地址结构
struct sockaddr_un myaddr;
bzero(&myaddr,sizeof(myaddr));
myaddr.run_family = PF_UNIX;
strcpy(myaddr.run_path,"/tmp/mysocket");//注意:mysocket为文件名

注意:如果文件系统已经存在该路径,则绑定的时候会失败,所以一般会先删除这个路径,另外地址结构必须以空字符结尾。

扩展函数

int access(const char *pathname,int mode);//查看文件是否有权限
//pathname : 文件路径
//mode : 权限,填写宏:F_OK文件存在、W_OK文件可写、R_OK文件可读、X_OK文件可执行
//返回值:如果有权限返回0,无权限返回-1
int unlink(const char *pathname);//删除文件,也可通过system("rm <pathname>");调用命令删除
//pathname : 文件路径
//返回值:成功返回0,失败返回-1,并置errno

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/un.h>

#define UN_PATH "/home/linux/socket"

int main(int argc,const char **argv)
{
    struct sockaddr_un un;
    int sockfd = socket(AF_UNIX,SOCK_STREAM,0);
    if(sockfd < 0) {
        perror("fail to sockfd");
        exit(EXIT_FAILURE);
    }

    if(access(UN_PATH,F_OK) != 0)
	{
		printf("服务器没打开!");
       exit(EXIT_FAILURE);
	}

    bzero(&un,sizeof(un));
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path,UN_PATH);

    int ret = connect(sockfd,(const struct sockaddr *)&un,sizeof(un));
    if(ret < 0) {
        perror("fail to connect");
        exit(EXIT_FAILURE);
    }

    char buf[128] = {0};
    while(1) {
        memset(buf,0,sizeof(buf));
        fgets(buf,sizeof(buf),stdin);
        buf[strlen(buf)-1] = '\0';
        send(sockfd,buf,strlen(buf),0);

        memset(buf,0,sizeof(buf));
        recv(sockfd,buf,sizeof(buf),0);
        printf("recv : %s\n",buf);
    }

    close(sockfd);
    return 0;
}

服务器:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/un.h>

#define UN_PATH "/home/linux/socket"

int main(int argc,const char **argv)
{
    struct sockaddr_un un,unc;
    int sockfd = socket(AF_UNIX,SOCK_STREAM,0);
    if(sockfd < 0) {
        perror("create sockfd error!");
        exit(EXIT_FAILURE);
    }

    if(access(UN_PATH,F_OK) == 0) {
		unlink(UN_PATH);
	}

    bzero(&un,sizeof(un));
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path,UN_PATH);

    if(bind(sockfd,(struct sockaddr *)&un,sizeof(un)) < 0) {
        perror("fail to bind");
        exit(EXIT_FAILURE);
    }

    if(listen(sockfd,1) < 0) {
        perror("fail to listen");
        exit(EXIT_FAILURE);
    }

    bzero(&unc,sizeof(unc));
    socklen_t unclen = sizeof(unclen);
    int connfd = accept(sockfd,NULL,NULL);
    if(connfd < 0) {
        perror("fail to accept");
        exit(EXIT_FAILURE);
    }

    char buf[128] = {0};
    while(1) {
        memset(buf,0,sizeof(buf));
        recv(connfd,buf,sizeof(buf),0);
        printf("recv : %s\n",buf);

        send(connfd,buf,strlen(buf),0);
    }

    close(connfd);
    close(sockfd);
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值