Linux系统编程5——Socket编程(网络通信)

前言

本文用于记录Linux Socket编程

一、套接字

Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
在这里插入图片描述
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
在这里插入图片描述

二、基础知识

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?**发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,**因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x03e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
大小端转换函数(主机字节序和网络字节序互相转换)

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntoh1(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
IP地址转换函数

p->表示点分十进制的字符串形式
to->到
n->表示network网络
int inet_pton(int af, const char *src, void *dst);
函数说明: 将字符串形式的点分十进制IP转换为大端模式的网络IP(整形4字节数)
参数说明:
	af: AF_INET
	src: 字符串形式的点分十进制的IP地址
	dst: 存放转换后的变量的地址
例如: 
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);

手工也可以计算:192.168.232.145, 先将4个正数分别转换为16进制数, 
192--->0xC0  168--->0xA8   232--->0xE8   145--->0x91
最后按照大端字节序存放: 0x91E8A8C0, 这个就是4字节的整形值.

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
函数说明: 网络IP转换为字符串形式的点分十进制的IP
参数说明:
af: AF_INET
src: 网络的整形的IP地址
dst: 转换后的IP地址,一般为字符串数组
size: dst的长度
返回值: 
成功--返回指向dst的指针
失败--返回NULL, 并设置errno

例如: IP地址为010aa8c0, 转换为点分十进制的格式:
01---->1    0a---->10   a8---->168   c0---->192
由于从网络中的IP地址是高端模式, 所以转换为点分十进制后应该为: 	
192.168.10.1

三、网络套接字函数

socket模型创建流程图
在这里插入图片描述

3.1、struct sockaddr

socket编程用到的重要的结构体:struct sockaddr
在这里插入图片描述

struct sockaddr结构说明:
struct sockaddr {
     sa_family_t sa_family;
     char  sa_data[14];
};

struct sockaddr_in结构:
struct sockaddr_in {
         sa_family_t    sin_family; /* address family: AF_INET */
         in_port_t      sin_port;   /* port in network byte order */
         struct in_addr sin_addr;   /* internet address */
};

   /* Internet address. */
struct in_addr {
     uint32_t  s_addr;     /* address in network byte order */
     };	 //网络字节序IP--大端模式
通过man 7 ip可以查看相关说明

3.2、socket函数

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:
	AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
	AF_INET6 与上面类似,不过是来用IPv6的地址
	AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
	SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
	SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
	SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
	SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
	SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:0 表示使用默认协议。
返回值:
	成功:返回指向新创建的socket的文件描述符
	失败:返回-1,设置errno

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
当调用socket函数以后, 返回一个文件描述符, 内核会提供与该文件描述符相对应的读和写缓冲区, 同时还有两个队列, 分别是请求连接队列和已连接队列.(管道是两个文件描述符一个缓冲区)
在这里插入图片描述

3.3、bind函数

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
	socket文件描述符
addr:
	构造出IP地址加端口号
addrlen:
	sizeof(addr)长度
返回值:
	成功返回0,失败返回-1, 设置errno

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。

3.4、listen函数

函数作用:将套接字由主动态变为被动态

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:
	socket文件描述符
backlog:
	排队建立3次握手队列和刚刚建立3次握手队列的链接数和

查看系统默认backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

3.5、accept函数

函数作用:获得一个链接,若当前没有链接则会阻塞等待

#include <sys/types.h> 		/* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
	socket文件描述符
addr:
	传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
	传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
	成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
我们的服务器程序结构是这样的:

while (1) {
	cliaddr_len = sizeof(cliaddr);
	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
	n = read(connfd, buf, MAXLINE);
	......
	close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。

3.6、connect函数

#include <sys/types.h> 					/* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
	socket文件描述符
addr:
	传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
	传入参数,传入sizeof(addr)大小
返回值:
	成功返回0,失败返回-1,设置errno

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。

3.7、读取/发送数据函数

ssize_t read(int fd, void *buf, size_t count);
ssize_t wrote(int fd, const void *buf, size_t count);
ssize_t recv_(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

对应recv和send这两个函数flags直接填0就可以了.

注意: 如果写缓冲区已满, write也会阻塞, read读操作的时候, 若读缓冲区没有数据会引起阻塞.
使用socket的API函数编写服务端和客户端程序的步骤图示:
在这里插入图片描述
可以使用netstat命令查看监听状态和链接状态
netstat命令:
a表示显示所有,
n表示显示的时候以数字的方式来显示
p表示显示进程信息(进程名和进程PID)

3.8 客户端服务器通信demo

//服务端程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>

int main()
{
	//创建socket
	//int socket(int domain, int type, int protocol);
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	if(lfd<0)
	{
		perror("socket error");
		return -1;
	}
	
	//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY); //表示使用本地任意可用IP
	int ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	if(ret<0)
	{
		perror("bind error");	
		return -1;
	}

	//监听
	//int listen(int sockfd, int backlog);
	listen(lfd, 128);

	//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
	int cfd = accept(lfd, (struct sockaddr *)&client, &len);  //len是一个输入输出参数
	//const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
	
	//获取client端的IP和端口
	char sIP[16];
	memset(sIP, 0x00, sizeof(sIP));
	printf("client-->IP:[%s],PORT:[%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));
	printf("lfd==[%d], cfd==[%d]\n", lfd, cfd);

	int i = 0;
	int n = 0;
	char buf[1024];

	while(1)
	{
		//读数据
		memset(buf, 0x00, sizeof(buf));
		n = read(cfd, buf, sizeof(buf));
		if(n<=0)
		{
			printf("read error or client close, n==[%d]\n", n);
			break;
		}
		printf("n==[%d], buf==[%s]\n", n, buf);	

		for(i=0; i<n; i++)
		{
			buf[i] = toupper(buf[i]);
		}

		//发送数据
		write(cfd, buf, n);
	}

	//关闭监听文件描述符和通信文件描述符
	close(lfd);
	close(cfd);
	
	return 0;
}


//客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
	//创建socket---用于和服务端进行通信
	int cfd = socket(AF_INET, SOCK_STREAM, 0);
	if(cfd<0)
	{
		perror("socket error");
		return -1;
	}

	//连接服务端
	//int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	struct sockaddr_in serv;
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
	printf("[%x]\n", serv.sin_addr.s_addr);
	int ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));
	if(ret<0)
	{
		perror("connect error");
		return -1;
	}	

	int n = 0;
	char buf[256];
	while(1)
	{
		//读标准输入数据
		memset(buf, 0x00, sizeof(buf));
		n = read(STDIN_FILENO, buf, sizeof(buf));
		
		//发送数据
		write(cfd, buf, n);

		//读服务端发来的数据
		memset(buf, 0x00, sizeof(buf));
		n = read(cfd, buf, sizeof(buf));
		if(n<=0)
		{
			printf("read error or server closed, n==[%d]\n", n);
			break;
		}
		printf("n==[%d], buf==[%s]\n", n, buf);
	}

	//关闭套接字cfd
	close(cfd);

	return 0;
}

多进程多线程实现服务器demo参考多进程/多线程demo

四、select函数

1、TCP状态简介

在这里插入图片描述
了解TCP状态转换图可以帮助开发人员查找问题.
说明: 上图中粗线表示主动方, 虚线表示被动方, 细线部分表示一些特殊情况。对于建立连接的过程客户端属于主动方, 服务端属于被动接受方(图的上半部分)而对于关闭(图的下半部分), 服务端和客户端都可以先进行关闭.处于ESTABLISHED状态的时候就可以收发数据了, 双方在通信过程当中一直处于ESTABLISHED状态, 数据传输期间没有状态的变化.

  • TIME_WAIT状态一定是出现在主动关闭的一方.
  • 主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。
  • 使用netstat -anp可以查看连接状态
    在这里插入图片描述

为什么需要2MSL?
原因之一: 让四次挥手的过程更可靠, 确保最后一个发送给对方的ACK到达;若对方没有收到ACK应答, 对方会再次发送FIN请求关闭, 此时在2MS时间内被动关闭方仍然可以发送ACK给对方.

原因之二: 为了保证在2MSL时间内, 不能启动相同的SOCKET-PAIR.TIME_WAIT一定是出现在主动关闭的一方, 也就是说2MSL是针对主动关闭一方来说的;由于TCP有可能存在丢包重传, 丢包重传若发给了已经断开连接之后相同的socket-pair(该连接是新建的, 与原来的socket-pair完 全相同, 双方使用的是相同的IP和端口), 这样会对之后的连接造成困扰, 严重可能引起程序异常.

测试: 启动服务端和客户端, 然后先关闭服务端, 再次启动服务端, 此时服务端报错: bind error: Address already in use; 若是先关闭的客户端, 再关闭的服务端, 此时启动服务端就不会报这个错误.
在这里插入图片描述
此时服务端使用的8888端口处于TIME_WAIT状态
在这里插入图片描述
为什么高并发的短连接生成的TIME_WAIT会导致服务器端口不够用?
只有主动关闭的一方才会进入TIME_WAIT状态,那这种情况也就是高并发连接都是服务端主动关闭。那么端口不够用就是文件描述符不够用了,因为文件描述符只有在从TIME_WAIT状态转换到CLOSE状态后才会真正被系统收回。TIME_WAIT状态会持续2MSL的时间才会转换到CLOSE状态,一般是1-4分钟。如果在这段时间内文件描述符都被用完了,而关闭的连接处于TIME_WAIT状态导致文件描述符并没有被真正释放,就会出现这种情况。

2、端口复用

解决上述端口复用的问题。

2.1、setsockopt函数

setsockopt函数
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
	setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(int));

函数参数解释可看《UNIX环境高级编程》
注:由于错误是bind函数报出来的,所以该函数的调用要放在bind之前,socket之后。

3、半关闭状态

半关闭状态指的是:如果一方close, 另一方没有close, 则认为是半关闭状态, 处于半关闭状态的 时候, 可以接收数据, 但是不能发送数据. 相当于把文件描述符的写缓冲区操作关闭了.
注意: 半关闭一定是出现在主动关闭的一方.

3.1、shutdown函数

长连接和端连接的概念:

  • 连接建立之后一直不关闭为长连接;

  • 连接收发数据完毕之后就关闭为短连接;

shutdown和close的区别:
shutdown能够把文件描述符上的读或者写操作关闭, 而close关闭文件描述符只是将连接的引用计数的值减1, 当减到0就真正关闭文件描述符了.如: 调用dup函数或者dup2函数可以复制一个文件描述符, close其中一个并不影响另一个文件描述符, 而shutdown就不同了, 一旦shutdown了其中一个文件描述符, 对所有的文件描述符都有影响 .

3.2、心跳包

如何检查与对方的网络连接是否正常??
一般心跳包用于长连接.

  • 方法1

    keepAlive = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));
    

    由于不能实时的检测网络情况, 一般不用这种方法

  • 方法2: 在应用程序中自己定义心跳包, 使用灵活, 能实时把控.

4、高并发服务器模型——select

多路IO复用技术:select,同时监听多个文件描述符,将监控的操作交给内核去处理。

数据类型fd_set: 文件描述符集合--本质是位图(关于集合可联想一个信号集sigset_t)
void FD_CLR(int fd, fd_set *set);
	将fd从set集合中清除.

int FD_ISSET(int fd, fd_set *set);
功能描述: 判断fd是否在集合中
返回值: 如果fd在set集合中, 返回1, 否则返回0.

void FD_SET(int fd, fd_set *set);
将fd设置到set集合中.

void FD_ZERO(fd_set *set);
初始化set集合.

int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函数介绍: 委托内核监控该文件描述符对应的读,写或者错误事件的发生.
参数说明: 
	nfds: 最大的文件描述符+1
	readfds: 读集合, 是一个传入传出参数
		传入: 指的是告诉内核哪些文件描述符需要监控
		传出: 指的是内核告诉应用程序哪些文件描述符发生了变化
	writefds: 写文件描述符集合(传入传出参数)
	execptfds: 异常文件描述符集合(传入传出参数)
	timeout: 
		NULL--表示永久阻塞, 直到有事件发生
		0 --表示不阻塞, 立刻返回, 不管是否有监控的事件发生
		>0--到指定事件或者有事件发生了就返回
	
返回值: 成功返回发生变化的文件描述符的个数
		失败返回-1, 并设置errno值.


select优点:

  • 一个进程可以支持多个客户端
  • select支持跨平台

select缺点:

  • 代码编写困难,会涉及到用户区到内核区的来回拷贝

  • 当客户端多个连接, 但少数活跃的情况, select效率较低(对socket进行扫描时是线性扫描,即采用轮询的方法)
    例如: 作为极端的一种情况, 3-1023文件描述符全部打开, 但是只有1023有发送数据, select就显得效率低下

  • 最大支持1024个客户端连接.select最大支持1024个客户端连接,不是由文件描述符表最多可以支持1024个文件描述符限制的,而是由FD_SETSIZE=1024限制的.FD_SETSIZE=1024 fd_set使用了该宏, 当然可以修改内核,然后再重新编译内核, 一般不建议这么做.

select函数demo

//IO多路复用技术select函数的使用 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>

int main()
{
	int i;
	int n;
	int lfd;
	int cfd;
	int ret;
	int nready;
	int maxfd;//最大的文件描述符
	char buf[FD_SETSIZE];
	socklen_t len;
	int maxi;  //有效的文件描述符最大值
	int connfd[FD_SETSIZE]; //有效的文件描述符数组
	fd_set tmpfds, rdfds; //要监控的文件描述符集
	struct sockaddr_in svraddr, cliaddr;

	//创建socket
	lfd = socket(AF_INET, SOCK_STREAM, 0);
	if(lfd<0)
	{
		perror("socket error");
		return -1;
	}

	//允许端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定bind
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	svraddr.sin_port = htons(8888);
	ret = bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));
	if(ret<0)
	{
		perror("bind error");
		return -1;
	}

	//监听listen
	ret = listen(lfd, 5);
	if(ret<0)
	{
		perror("listen error");
		return -1;
	}

	//文件描述符集初始化
	FD_ZERO(&tmpfds);
	FD_ZERO(&rdfds);

	//将lfd加入到监控的读集合中
	FD_SET(lfd, &rdfds);

	//初始化有效的文件描述符集, 为-1表示可用, 该数组不保存lfd
	for(i=0; i<FD_SETSIZE; i++)
	{
		connfd[i] = -1;
	}

	maxfd = lfd;
	len = sizeof(struct sockaddr_in);

	//将监听文件描述符lfd加入到select监控中
	while(1)
	{
		//select为阻塞函数,若没有变化的文件描述符,就一直阻塞,若有事件发生则解除阻塞,函数返回
		//select的第二个参数tmpfds为输入输出参数,调用select完毕后这个集合中保留的是发生变化的文件描述符
		tmpfds = rdfds;
		nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
		if(nready>0)
		{
			//发生变化的文件描述符有两类, 一类是监听的, 一类是用于数据通信的
			//监听文件描述符有变化, 有新的连接到来, 则accept新的连接
			if(FD_ISSET(lfd, &tmpfds))	
			{
				cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);			
				if(cfd<0)
				{
					if(errno==ECONNABORTED || errno==EINTR)
					{
						continue;
					}
					break;
				}

				//先找位置, 然后将新的连接的文件描述符保存到connfd数组中
				for(i=0; i<FD_SETSIZE; i++)
				{
					if(connfd[i]==-1)
					{
						connfd[i] = cfd;
						break;
					}
				}
				//若连接总数达到了最大值,则关闭该连接
				if(i==FD_SETSIZE)
				{	
					close(cfd);
					printf("too many clients, i==[%d]\n", i);
					//exit(1);
					continue;
				}

				//确保connfd中maxi保存的是最后一个文件描述符的下标
				if(i>maxi)
				{
					maxi = i;
				}

				//打印客户端的IP和PORT
				char sIP[16];
				memset(sIP, 0x00, sizeof(sIP));
				printf("receive from client--->IP[%s],PORT:[%d]\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, sIP, sizeof(sIP)), htons(cliaddr.sin_port));

				//将新的文件 描述符加入到select监控的文件描述符集合中
				FD_SET(cfd, &rdfds);
				if(maxfd<cfd)
				{
					maxfd = cfd;
				}

				//若没有变化的文件描述符,则无需执行后续代码
				if(--nready<=0)
				{
					continue;
				}	
			}

			//下面是通信的文件描述符有变化的情况
			//只需循环connfd数组中有效的文件描述符即可, 这样可以减少循环的次数
			for(i=0; i<=maxi; i++)
			{
				int sockfd = connfd[i];
				//数组内的文件描述符如果被释放有可能变成-1
				if(sockfd==-1)
				{
					continue;
				}

				if(FD_ISSET(sockfd, &tmpfds))
				{
					memset(buf, 0x00, sizeof(buf));
					n = read(sockfd, buf, sizeof(buf));
					if(n<0)
					{
						perror("read over");
						close(sockfd);
						FD_CLR(sockfd, &rdfds);
						connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
					}
					else if(n==0)
					{
						printf("client is closed\n");	
						close(sockfd);
						FD_CLR(sockfd, &rdfds);
						connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
					}
					else
					{
						printf("[%d]:[%s]\n", n, buf);
						write(sockfd, buf, n);
					}

					if(--nready<=0)
					{
						break;  //注意这里是break,而不是continue, 应该是从最外层的while继续循环
					}
				}	
			}
		}	
	}

	//关闭监听文件描述符
	close(lfd);

	return 0;
}

五、poll函数

多路IO-poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数说明: 跟select类似, 监控多路IO, 但poll不能跨平台.
参数说明:
	fds: 传入传出参数, 实际上是一个结构体数组
		fds.fd: 要监控的文件描述符
		fds.events: 
			POLLIN---->读事件
			POLLOUT---->写事件
		fds.revents: 返回的事件
	nfds: 数组实际有效内容的个数
	timeout: 超时时间, 单位是毫秒.
		-1:永久阻塞, 直到监控的事件发生
		0: 不管是否有事件发生, 立刻返回
		>0: 直到监控的事件发生或者超时
	返回值: 
		成功:返回就绪事件的个数
		失败: 返回-1
		若timeout=0, poll函数不阻塞,且没有事件发生, 此时返回-1, 并且errno=EAGAIN, 这种情	
			况不应视为错误.
struct pollfd 
{
   int   fd;        /* file descriptor */   监控的文件描述符
   short events;     /* requested events */  要监控的事件---不会被修改
   short revents;    /* returned events */   返回发生变化的事件 ---由内核返回
};

说明:

  1. 当poll函数返回的时候, 结构体当中的fd和events没有发生变化, 究竟有没有事件发生由revents来判断,所以poll是请求和返回分离.
  2. struct pollfd结构体中的fd成员若赋值为-1, 则poll不会监控.
  3. 相对于select, poll没有本质上的改变; 但是poll可以突破1024的限制.

/proc/sys/fs/file-max查看一个进程可以打开的socket描述符上限.如果需要可以修改配置文件: /etc/security/limits.conf加入如下配置信息, 然后重启终端即可生效.

  • soft nofile 1024
  • hard nofile 100000

soft和hard分别表示ulimit命令可以修改的最小限制和最大限制
poll demo(wrap.h 在这)

//IO多路复用技术poll函数的使用 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include "wrap.h"

int main()
{
	int i;
	int n;
	int lfd;
	int cfd;
	int ret;
	int nready;
	int maxfd;
	char buf[1024];
	socklen_t len;
	int sockfd;
	fd_set tmpfds, rdfds;
	struct sockaddr_in svraddr, cliaddr;
	
	//创建socket
	lfd = Socket(AF_INET, SOCK_STREAM, 0);

	//允许端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));

	//绑定bind
	svraddr.sin_family = AF_INET;
	svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	svraddr.sin_port = htons(8888);
	ret = Bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));

	//监听listen
	ret = Listen(lfd, 128);

	struct pollfd client[1024];
	for(i=0; i<1024; i++)
	{
		client[i].fd = -1;
	}		

	//将监听文件描述符委托给内核监控----监控读事件
	client[0].fd = lfd;
	client[0].events = POLLIN;

	maxfd = 0; //maxfd表示内核监控的范围

	while(1)
	{
		nready = poll(client, maxfd+1, -1);
		if(nready<0)
		{
			perror("poll error");
			exit(1);
		}
		
		//有客户端连接请求
		if(client[0].fd==lfd && (client[0].revents & POLLIN))
		{
			cfd = Accept(lfd, NULL, NULL);

			//寻找client数组中的可用位置
			for(i=1; i<1024; i++)
			{
				if(client[i].fd==-1)
				{
					client[i].fd = cfd;
					client[i].events = POLLIN;
					break;
				}
			}

			//若没有可用位置, 则关闭连接
			if(i==1024)
			{
				Close(cfd);
				continue;
			}

			if(maxfd<i)
			{
				maxfd = i;
			}
			
			if(--nready==0)
			{
				continue;
			}
		}

		//下面是有数据到来的情况
		for(i=1; i<=maxfd; i++)
		{
			//若fd为-1, 表示连接已经关闭或者没有连接
			if(client[i].fd==-1)	
			{
				continue;
			}
			/*
				如果不在此处判断,链接多个客户端时就会对i进行遍历,Read函数会阻塞
			*/
			if(client[i].revents == POLL_IN){
				sockfd = client[i].fd;
				memset(buf, 0x00, sizeof(buf));
				n = Read(sockfd, buf, sizeof(buf));
				if(n<=0)
				{
					printf("read error or client closed,n==[%d]\n", n);
					Close(sockfd);
					client[i].fd = -1; //fd为-1,表示不再让内核监控
				}
				else
				{
					printf("read over,n==[%d],buf==[%s]\n", n, buf);
					write(sockfd, buf, n);
				}
	
				if(--nready==0)
				{
					break;
				}
			}
		}

	}


	Close(lfd);
	return 0;
}

六、epoll函数

将检测文件描述符的变化委托给内核去处理, 然后内核将发生变化的文件描述符对应的事件返回给应用程序.可参考

函数介绍:
int epoll_create(int size);
函数说明: 创建一个树根
参数说明:
	size: 最大节点数, 此参数在linux 2.6.8已被忽略, 但必须传递一个大于0的数.
返回值:
	成功: 返回一个大于0的文件描述符, 代表整个树的树根.
	失败: 返回-1, 并设置errno值.

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数说明: 将要监听的节点在epoll树上添加, 删除和修改
参数说明:
	epfd: epoll树根
	op:
		EPOLL_CTL_ADD: 添加事件节点到树上
		EPOLL_CTL_DEL: 从树上删除事件节点
		EPOLL_CTL_MOD: 修改树上对应的事件节点
	fd: 事件节点对应的文件描述符
	event: 要操作的事件节点
     typedef union epoll_data {
              void        *ptr;
			  int          fd;
              uint32_t     u32;
              uint64_t     u64;
        } epoll_data_t;

     struct epoll_event {
             uint32_t     events;      /* Epoll events */
             epoll_data_t data;        /* User data variable */
           };
	event.events常用的有:
		 EPOLLIN: 读事件
		 EPOLLOUT: 写事件
 		 EPOLLERR: 错误事件
         EPOLLET: 边缘触发模式
	event.fd: 要监控的事件对应的文件描述符
	
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数说明:等待内核返回事件发生
参数说明:
	epfd: epoll树根
	events: 传出参数, 其实是一个事件结构体数组
	maxevents: 数组大小
	timeout:
		-1: 表示永久阻塞
		0: 立即返回
		>0: 表示超时等待事件
	 返回值:
	成功: 返回发生事件的个数
	失败: 若timeout=0, 没有事件发生则返回; 返回-1, 设置errno值, 
epoll_wait的events是一个传出参数, 调用epoll_ctl传递给内核什么值, 当epoll_wait返回
的时候, 内核就传回什么值,不会对struct event的结构体变量的值做任何修改.

epoll代码demo

1、epoll的两种工作模式

epoll的两种模式ET和LT模式

  • 水平触发: 高电平代表1

    只要缓冲区中有数据, 就一直通知

  • 边缘触发: 电平有变化就代表1

    缓冲区中有数据只会通知一次, 之后再有数据才会通知.(若是读数据的时候没有读完, 则剩余的数据不会再通知, 直到有新的数据到来)

    边缘非阻塞模式: 提高效率
    ET模式由于只通知一次, 所以在读的时候要循环读, 直到读完, 但是当读完之后read就会阻塞, 所以应该将该文件描述符设置为非阻塞模式(fcntl函数).read函数在非阻塞模式下读的时候, 若返回-1, 且errno为EAGAIN, 则表示当前资源不可用, 也就是说缓冲区无数据(缓冲区的数据已经读完了); 或者当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲区中已没有数据可读了,也就可以认为此时读事件已处理完成。

2、epoll反应堆

反应堆: 一个小事件触发一系列反应.
epoll反应堆的思想: c++的封装思想(把数据和操作封装到一起)

–将描述符,事件,对应的处理方法封装在一起
–当描述符对应的事件发生了, 自动调用处理方法(其实原理就是回调函数)

 typedef union epoll_data {
               void        *ptr;
               int uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

  struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

epoll反应堆的核心思想是: 在调用epoll_ctl函数的时候, 将events上树的时候,利用epoll_data_t的ptr成员, 将一个文件描述符,事件和回调函数封装成一个结构体, 然后让ptr指向这个结构体, 然后调用epoll_wait函数返回的时候, 可以得到具体的events, 然后获得events结构体中的events.data.ptr指针, ptr指针指向的结构体中有回调函数, 最终可以调用这个回调函数.

七、select、poll、epoll函数的区别

1、buffer and cache

buffer: A buffer is something that has yett to be "written" to disk
cache: A cache is something that has been "read" from the disk and stored for later use

即buffer用于存储用输出到磁盘上的数据,而cache是从磁盘读出存放到内存中待今后使用的数据。它们的引入均是为了提高IO的性能
在这里插入图片描述

上图是我的笔记本上虚拟机centos7的内存使用情况。

total = used + free + buff/cache
available = free + buff/cache

这里需要解释的是free -m这个命令观察角度,即

Memory that isYou’d call itLinux calls it
taken by applicationsUsedUsed
available for applications, and used for some thingFreeUsed
not used for anythingFreeFree

表中something代表的正是free命令中"buffer/cached"的内存,由于这块内存从操作系统的角度确实被使用。但如果用户要使用,这块内存是可以很快被回收被用户程序所使用,因此从用户的角度这块内存应该划为空闲状态。Linux并没有吃掉你的内存,只要还未使用到交换分区,你的内存所剩无几时,Linux缓存了大量的数据,也许下一次就从中受益。

2、内核缓冲区(Kernel Buffer Cache)

在这里插入图片描述
在介绍内核缓冲区前我们先看一下应用程序是如何从磁盘读取文件的,通常是分两步走。第一步是从磁盘上读取数据存到内核空间,第二步把数据从内核空间拷贝到用户空间。所以这个过程有两次操作:

  • 第一步:从磁盘上读取
  • 第二步:从内存中读取
    很明显第二步的速度远大于第一步,性能的瓶颈就在第一步。为了解决这个问题,内核缓冲区就应运而生了。其本质上也就是内核空间中的一块内存。

先看看是如何充当Cache的
数据预读:当用户编写的应用程序发起read()系统调用时(这个时候从用户态切换到内核态),内核会比请求从磁盘上读取更多的数据,保存在缓冲区,以备程序的后续使用。这种数据的预读取策略其实就是基于局部性原理。因此当我们向内核请求读取数据时,内核会先到内核缓冲区中去寻找,如果命中数据,则不需要进行真正的磁盘 I/O,直接从缓冲区中返回数据就行了;如果缓存未命中,则内核会从磁盘中读取请求的 page,并同时读取紧随其后的几个 page(比如三个),如果文件是顺序访问的,那么下一个读取请求就会命中之前预读的缓存(当然了,预读算法非常复杂,这里只是一个简化的逻辑)

再看看内核缓冲区是如何充当Buffer的:
延时回写:回写指的是,当程序发起 write() 系统调用时,内核并不会直接把数据写入到磁盘文件中,而仅仅是写入到缓冲区中,几秒后(或者说等数据堆积了一些后)才会真正将数据刷新到磁盘中。对于系统调用来说,数据写入缓冲区后,就返回了。
延迟往磁盘写入数据的最大一个好处就是,可以合并更多的数据一次性写入磁盘,把小块的 I/O 变成大块 I/O,减少磁盘处理命令次数,从而提高提盘性能。
另一个好处是,当其它进程紧接着访问该文件时,内核可以从直接从缓冲区中提供更新的文件数据(这里又是充当 Cache 了)。

说起来一大堆,其实很简单,把握缓冲和缓存的定义就行了,如果你是读,我就会拿多一点放在内核缓冲区,这样你下次读的时候大概率就不需要访问磁盘了,直接从内核缓冲区拿就行;如果你是写,我就会等内核缓冲区中的数据堆积得多了再写磁盘,而不是来一点数据就写一次磁盘无论是读操作还是写操作,无论是充当缓存还是缓冲,究其根本,内核缓冲区的作用都是为了减少磁盘 IO 的次数。

3、用户缓冲区

通过上面我们知道了内核缓冲区可以帮助用户减少磁盘IO的次数,但是每次系统调用都需要从用户态切换到内核态(这也是挺费时的,但是远好于直接访问磁盘)。那么为了进一步减少系统调用的发生,就又设计了用户缓冲区。作用和内核缓冲区一样,数据阅读 + 延时回写,既充当 Cache 又充当 Buffer。

不同的就是,内核缓冲区处理的是内核空间和磁盘之间的数据传递,目的是减少访问磁盘的次数;而用户缓冲区处理的是用户空间和内核空间的数据传递,目的是减少系统调用的次数。
在这里插入图片描述
另外,从上面的分析我们可以看出,read() 和 write() 都并非真正执行 I/O 操作(或者说,都并不直接和磁盘进行交互),它只代表数据在用户空间 / 内核空间传递的完成,read 是把数据从内核缓冲区复制到用户缓冲区,write 是把用户缓冲区复制到内核缓冲区。

4、IO模型

4.1 阻塞、非阻塞、同步与异步

针对IO的操作,可以分成两个阶段,准备阶段和操作阶段

  1. 准备阶段:判断是否能够操作(即等待数据是否可用),是内核进程完成的。
  2. 操作阶段:执行实际的IO调用,数据从内核缓冲区拷贝到用户进程缓冲区中。

阻塞与非阻塞的概念:
阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态。阻塞与挂起的区别与联系
联系上面IO操作的两个阶段,我们可以知道阻塞IO是指IO操作需要彻底完成后才返回到用户空间,即上面两步都是阻塞的。非阻塞IO是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。即在第一阶段,程序不断的轮询直到数据准备好,第2阶段还是阻塞的。(可以发现,无论是阻塞IO还是非阻塞IO,第二步都是阻塞的,两者的区别在于第一步,即阻塞IO会一直等待,而非阻塞IO会返回用户空间干其他的事,定时的切换回内核态查看第一步是否完成)。
同步与异步的概念:
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)。所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
同步IO和异步IO的区别在于将数据从内核复制到用户空间时,用户进程是否会阻塞。阻塞为同步,非阻塞为异步。即上面的第一步都不阻塞,看第二步是否阻塞。

4.2、IO模型分类

《Unix网络编程卷1:套接字联网API》(即UNP)中第六章对unix 系统将IO模型分为五类:阻塞IO,非阻塞IO,IO复用,信号驱动,异步IO。

  1. 阻塞IO:在准备阶段即同步阻塞,应用进程调用I/O操作时阻塞,只有等待要操作的数据准备好,并复制到应用进程的缓冲区中才返回;

  2. 非阻塞IO:当应用进程要调用的I/O操作会导致该进程进入阻塞状态时,该I/O调用返回一个错误,一般情况下,应用进程需要利用轮询的方式来检测某个操作是否就绪。数据就绪后,实际的I/O操作会等待数据复制到应用进程的缓冲区中以后才返回;

  3. IO复用:多路IO共用一个同步阻塞接口,任意IO可操作都可激活IO操作,这是对阻塞IO的改进(主要是select和poll、epoll,关键是能实现同时对多个IO端口进行监听)。此时阻塞发生在select/poll的系统调用上,而不是阻塞在实际的I/O系统调用上。IO多路复用的高级之处在于:它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select等函数就可以返回。

  4. 信号驱动IO:注册一个IO信号事件,在数据可操作时通过SIGIO信号通知线程;

  5. 异步IO: 应用进程通知内核开始一个异步I/O操作,并让内核在整个操作(包含将数据从内核复制到应该进程的缓冲区)完成后通知应用进程。

前四种模型在第一阶段即判断是否可操作阶段各不相同,但一旦数据可操作,则切换到同步阻塞模式下执行IO操作,所以都算是同步IO。

根据上面所说的IO操作的两个阶段,可以把上面的I/O模型进行如下归类:

  • 1、阻塞IO:在两个阶段上面都是阻塞的;

  • 2、非阻塞IO:在第1阶段,程序不断的轮询直到数据准备好(期间在用户态和内核态来回切换),第2阶段还是阻塞的;

  • 3、IO复用:在第1阶段,当一个或者多个IO准备就绪时,通知程序,第2阶段还是阻塞的,在第1阶段还是轮询实现的,只是所有的IO都集中在一个地方,这个地方进行轮询;

  • 4、信号IO:当数据准备完毕的时候,信号通知程序数据准备完毕,第2阶段阻塞;

  • 5、异步IO:1,2都不阻塞

阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型,这三种模型的区别在于第一阶段(阻塞式I/O阻塞在I/O操作上,非阻塞式I/O轮询,I/O复用阻塞在select/poll/epoll上),第二阶段都是一样的,即这里的阻塞不阻塞体现在第一阶段,从这方面来说I/O复用类型也可以归类到阻塞式I/O,它与阻塞式I/O的区别在于阻塞的系统调用不同。而异步I/O的两个阶段都不会阻塞进程。
在这里插入图片描述

其中POSIX将IO只分成了同步IO、异步IO两种模型

  • 同步I/O操作:实际的I/O操作将导致请求进程阻塞,直到I/O操作完成。
  • 异步I/O操作:实际的I/O操作不导致请求进程阻塞。

由此,前面分类中:阻塞式I/O,非阻塞式I/O,I/O复用,信号驱动I/O模型都属于同步I/O,因为第二阶段的数据复制都是阻塞的。而只有前面定义的异步I/O模型属于这里的异步I/O操作。
说了这么多,来个例子:
假设同学们需要拿杯子去水龙头接水,需要满足两个条件才能回来:

  1. 水龙头有水——内核中数据准备阶段
  2. 杯子接满水——数据从内核拷贝到用户空间

A同学拿了杯子去接水,发现水龙头没水,在那等待,过了一会,水龙头有水了,将杯子接满了,返回——阻塞IO;
B同学拿了杯子去接水,发现水龙头没水,返回(用户态),过了一会再去看(内核态),有水,拿杯子接满水,返回——非阻塞IO;
C同学去接水的时候发现有一排水龙头(IO复用),并且宿管阿姨告诉他,这些水龙头都没水,有水的时候自己会通知他。于是C同学等啊等(select调用中),宿管阿姨告诉他来水了,但是不知道哪个水龙头有。C同学自己从第一个水龙头开始试,找到有水的并开始装水(select水龙头有限,poll水龙头无限,epoll水龙头无限并且阿姨会告诉哪几个有水)——IO复用;
D同学让宿管阿姨水龙头有水的时候告诉他(注册信号函数),过了一会儿D同学被通知有水了,自己拿杯子去接水了(注意不是异步,因为水还是自己接的)——信号驱动IO;
E同学让宿管阿姨接好水后告诉他(E同学没有做任何事)——异步IO

5、select、poll、epoll三者区别

这篇博客写的非常清楚,这里就拿来主义了。

八、UDP通信

TCP:传输控制协议, 面向连接的,稳定的,可靠的,安全的数据流传递

稳定和可靠: 丢包重传
数据有序: 序号和确认序号
流量控制: 滑动窗口 

UDP:用户数据报协议

面向无连接的,不稳定,不可靠,不安全的数据报传递---更像是收发短信
UDP传输不需要建立连接,传输效率更高,在稳定的局域网内环境相对可靠

1、UDP通信相关函数

1.1、recvfrom函数

函数作用:接受消息

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设置error
注:1、调用该函数相当于TCP通信的recv+accept函数
    2、若没有收到数据,则一直阻塞

1.2、sendto函数

函数作用:发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
	sockfd:套接字
	dest_addr: 目的地址
	addrlen: 目的地址长度
返回值:
	成功:返回写入的字节数
	失败:返回-1,设置errno

在这里插入图片描述

UDP的服务器编码流程:
	创建套接字  type=SOCK_DGRAM
	绑定ip和端口 
	while(1)
    {
		收发消--recvfrom
		发消息--sendto
    }
	关闭套接字--close

UDP客户端流程:
	创建套接字--socket
	while(1)
    {
		收发消--recvfrom
		发消息--sendto
    }
	关闭套接字--close

demo

//udp服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>

int main()
{
	//创建socket
	int cfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(cfd<0)
	{
		perror("socket error");
		return -1;
	}

	//绑定
	struct sockaddr_in serv;
	struct sockaddr_in client;
	bzero(&serv, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(cfd, (struct sockaddr *)&serv, sizeof(serv));

	int i;
	int n;
	socklen_t len;
	char buf[1024];
	while(1)
	{
		//读取数据
		memset(buf, 0x00, sizeof(buf));
		len = sizeof(client);
		n = recvfrom(cfd, buf, sizeof(buf), 0, (struct sockaddr *)&client, &len);

		//将大写转换为小写
		for(i=0; i<n; i++)
		{
			buf[i] = toupper(buf[i]);
		}
		printf("[%d]:n==[%d], buf==[%s]\n", ntohs(client.sin_port), n, buf);
		//发送数据
		sendto(cfd, buf, n, 0, (struct sockaddr *)&client, len);
	}

	//关闭套接字
	close(cfd);

	return 0;
}
//udp客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>

int main()
{
	//创建socket
	int cfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(cfd<0)
	{
		perror("socket error");
		return -1;
	}

	int n;
	char buf[1024];
	struct sockaddr_in serv;
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);

	while(1)
	{
		//读标准输入数据
		memset(buf, 0x00, sizeof(buf));
		n = read(STDIN_FILENO, buf, sizeof(buf));

		//发送数据
		sendto(cfd, buf, n, 0, (struct sockaddr *)&serv, sizeof(serv));

		//读取数据
		memset(buf, 0x00, sizeof(buf));
		n = recvfrom(cfd, buf, sizeof(buf), 0, NULL, NULL);
		printf("n==[%d], buf==[%s]\n", n, buf);
	}

	//关闭套接字
	close(cfd);

	return 0;
}

```c

references

虎牙一面:请详细介绍一下内核缓冲区
深入理解I/O的概念及其区别
阻塞与挂起
同步、异步、阻塞、非阻塞、异步IO

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很高兴能够回答你的问题。以下是一个简单的即时通信系统的C++网络编程示例,基于Linux平台使用socket库进行开发。 客户端代码: ```c++ #include <iostream> #include <cstring> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; int main() { // 创建socket int client_socket = socket(AF_INET, SOCK_STREAM, 0); // 配置服务器地址 sockaddr_in server_address{}; server_address.sin_family = AF_INET; server_address.sin_port = htons(8080); server_address.sin_addr.s_addr = inet_addr("127.0.0.1"); // 连接服务器 int connect_result = connect(client_socket, (sockaddr *) &server_address, sizeof(server_address)); if (connect_result == -1) { cout << "连接服务器失败!" << endl; return -1; } // 发送消息 char message[1024] = {0}; while (true) { cin.getline(message, sizeof(message)); if (strlen(message) == 0) { continue; } send(client_socket, message, strlen(message), 0); memset(message, 0, sizeof(message)); } // 关闭socket close(client_socket); return 0; } ``` 服务器端代码: ```c++ #include <iostream> #include <cstring> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; int main() { // 创建socket int server_socket = socket(AF_INET, SOCK_STREAM, 0); // 配置服务器地址 sockaddr_in server_address{}; server_address.sin_family = AF_INET; server_address.sin_port = htons(8080); server_address.sin_addr.s_addr = inet_addr("127.0.0.1"); // 绑定socket和地址 bind(server_socket, (sockaddr *) &server_address, sizeof(server_address)); // 监听客户端连接 listen(server_socket, 10); // 接受客户端连接 sockaddr_in client_address{}; socklen_t client_address_len = sizeof(client_address); int client_socket = accept(server_socket, (sockaddr *) &client_address, &client_address_len); // 接收消息 char message[1024] = {0}; while (true) { int recv_result = recv(client_socket, message, sizeof(message), 0); if (recv_result <= 0) { break; } cout << "收到消息:" << message << endl; memset(message, 0, sizeof(message)); } // 关闭socket close(client_socket); close(server_socket); return 0; } ``` 在上面的代码中,客户端和服务器端都使用了socket库中的函数来完成网络通信。客户端使用`socket`函数创建socket,使用`connect`函数连接服务器,并使用`send`函数发送消息。服务器端使用`socket`函数创建socket,使用`bind`函数将socket与服务器地址绑定,使用`listen`函数监听客户端连接,并使用`accept`函数接受客户端连接,最后使用`recv`函数接收客户端发送的消息。 当然,这只是一个简单的示例。在实际开发中,你需要考虑更多的细节,例如如何处理多个客户端连接、如何处理异常情况等等。 希望这个示例能够帮助你了解Linux下C++ socket网络编程的基本流程和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值