Skip to content

LV035-多路复用IO-01-select ​

本文主要是网络编程——多路复用IO中的select的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、select相关函数 ​

1. select() ​

1.1 函数说明 ​

在linux下可以使用man 2 select命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

【函数说明】该函数用于执行I/O多路复用操作,能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

【函数参数】

  • nfds:int类型,通常表示需要监视的最大文件描述符编号值加1。

  • readfds:fd_set类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符读是否就绪(是否可读)。select()返回后,readfds将清除除准备读取的文件描述符外的所有文件描述符。

  • writefds:fd_set类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符写是否就绪(是否可写)。select()返回后,writefds将清除除准备写入的文件描述符外的所有文件描述符。

  • exceptfds:fd_set类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符是否出现异常,需要注意异常情况并不是在文件描述符上出现了一些错误。select()返回后,exceptfds将清除除了发生异常条件的文件描述符以外的所有文件描述符。

  • timeout:struct timeval类型结构体指针变量,用于设定select()阻塞的时间上限,控制select的阻塞行为。在使用man手册的时候,会有 timeout 结构体的说明,该结构体的成员如下:

c
struct timeval
{
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
};

(1)我们可将timeout参数设置为NULL,表示select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态;

(2)如果参数timeout指向一个struct timeval结构体对象,并且这个结构体对象中的两个成员变量都为0,那么此时select()函数不会阻塞,它只是简单地轮训指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。

(3)若参数timeout将为select()指定一个等待(阻塞)时间的上限值,如果在阻塞期间内,文件描述符集合中的某一个或多个文件描述符成为就绪态,将会结束阻塞并返回;如果超过了阻塞时间的上限值,select()函数将会返回。

【返回值】int类型,返回值可能会有以下三种情况:

  • 返回-1:表示有错误发生,并且会设置errno。可能的错误码包括EBADF、EINTR、EINVAL、EINVAL以及ENOMEM,其中EBADF表示readfds、writefds或exceptfds中有一个文件描述符是非法的;EINTR表示该函数被信号处理函数中断了,其他更详细的我们可以使用man命令查看帮助手册去详细了解。
  • 返回0:表示在任何文件描述符成为就绪态之前select()调用已经超时,在这种情况下,readfds,writefds以及exceptfds所指向的文件描述符集合都会被清空。
  • 返回正整数:表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过FD_ISSET()宏进行检查,以此查找发生的I/O事件是什么。如果同一个文件描述符在readfds,writefds以及exceptfds中同时被指定,且它有多个I/O事件都处于就绪态的话,那么这个文件描述符就会被统计多次,换句话说,也就是说select()返回三个集合中被标记为就绪态的文件描述符的总数。

【使用格式】后边还有几个知识点,到最后查看使用实例即可。

c
/* 需要的一些头文件 */
#include <sys/select.h>
/* 需要的一些语句步骤 */
fd_set rset;
int maxfd = -1;

struct timeval tout;

fd = socket ( ...);
bind (fd, ...);
listen (fd, ...);

while (1)
{
	maxfd = fd;
	FD_ZERO (&rset);

	FD_SET (fd, &rset);
	//依次把已经建立好连接fd加入到集合中,记录下来最大的文件描述符maxfd
	//...FIXME!!
#if 0
	select (maxfd + 1, &rset, NULL, NULL, NULL);
#else
	tout.tv_sec = 5;
	tout.tv_usec = 0;
	select (maxfd + 1, &rset, NULL, NULL, &tout);
#endif
	if (FD_ISSET (fd, &rset))
	{
		newfd = accept (fd, ....);
	}
	//依次判断已建立连接的客户端是否有数据
	//...FIXME!

}

【注意事项】

(1)select()函数将阻塞直到有以下事情发生:

  • readfds、writefds或exceptfds指定的文件描述符中至少有一个成为就绪态(变为可读、可写或者发生了异常)。
  • 该调用被信号处理函数中断。
  • 参数timeout中指定的时间上限已经超时。

(2)理论上说,nfds参数应该不会超过1025,对于我现在使用的Ubuntu来说,之前有查询过一个进程最多可以打开1024个文件描述符,所以这里应该不会超过1025。

(3)一般情况,我们会使用读集合。写集合填NULL,这是因为我们写的时候一般都是读取完时候后给到一个反馈,然后会写入数据,所以这里写集合我们一般不怎么关心。异常集合一般也是是填NULL,有些特殊的情况可能会用到,但是吧,我还没遇到过,就先这样吧,后边遇到了再补充。

(4) select()函数里面的各个文件描述符fd_set集合的参数在select()前后发生了变化:

  • 前:表示关心的文件描述符集合。

  • 后:有数据的集合(如不是在超时还回情况下)。谁改变了这个集合?答案是kernel,就是内核。

(5)调用select()函数之后,select()函数会清空它所检测的socket描述符集合,导致每次调用select()之前都必须把socket描述符重新加入到待检测的集合中。

1.2 使用实例 ​

暂无。

2. pselect() ​

在我们使用man手册查询select()函数相关用法的时候,一定看到过这个函数,下边是这个函数的一些相关笔记。

2.1 函数说明 ​

在linux下可以使用man 2 pselect命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);

【函数说明】该函数用于执行I/O多路复用操作,该函数是一个 防止信号干扰的增强型 select()函数,它也能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

【函数参数】

  • nfds:int类型,通常表示需要监视的最大文件描述符编号值加1。

  • readfds:fd_set类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符读是否就绪(是否可读)。select()返回后,readfds将清除除准备读取的文件描述符外的所有文件描述符。

  • writefds:fd_set类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符写是否就绪(是否可写)。select()返回后,writefds将清除除准备写入的文件描述符外的所有文件描述符。

  • exceptfds:fd_set类型指针变量,指向一个文件描述符集合,用来检测这个文件描述符集合中的文件描述符是否出现异常,需要注意异常情况并不是在文件描述符上出现了一些错误。select()返回后,exceptfds将清除除了发生异常条件的文件描述符以外的所有文件描述符。

  • timeout:struct timespec类型结构体指针变量,用于设定pselect()阻塞的时间上限,与select()函数的timeout参数作用相同,只是时间精度不同。在使用man手册的时候,会有 timeout 结构体的说明,该结构体的成员如下:

c
struct timespec
{
	time_t      tv_sec;         /* seconds */
	long        tv_nsec;        /* nanoseconds */
};

这个参数与select()的timeout参数作用相同,只是pselect()函数使用的是struct timespec结构体,可以指定超时时间到纳秒级。

  • sigmask:sigset_t类型指针变量,表示信号掩码,指定一个需要屏蔽的信号集。

【返回值】int类型,返回值与select()函数相同,可能会有以下三种情况:

  • 返回-1:表示有错误发生,并且会设置errno。可能的错误码包括EBADF、EINTR、EINVAL、EINVAL以及ENOMEM,其中EBADF表示readfds、writefds或exceptfds中有一个文件描述符是非法的;EINTR表示该函数被信号处理函数中断了,其他更详细的我们可以使用man命令查看帮助手册去详细了解。
  • 返回0:表示在任何文件描述符成为就绪态之前select()调用已经超时,在这种情况下,readfds,writefds以及exceptfds所指向的文件描述符集合都会被清空。
  • 返回正整数:表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过FD_ISSET()宏进行检查,以此查找发生的I/O事件是什么。如果同一个文件描述符在readfds,writefds以及exceptfds中同时被指定,且它有多个I/O事件都处于就绪态的话,那么这个文件描述符就会被统计多次,换句话说,也就是说select()返回三个集合中被标记为就绪态的文件描述符的总数。

【使用格式】暂时还没有用到过,后边遇到了再补充。

【注意事项】none

2.2 sigmask参数说明 ​

sigmask参数指定了一个应该在pselect()函数调用期间被阻塞的信号的集合,它会在调用期间覆盖当前的信号掩码,当函数返回后恢复之前的信号掩码。如果sigmask为NULL,信号掩码在pselect()调用期间不会被修改。(信号掩码是啥?进程通信部分里边关于信号阻塞的部分内容)。在man手册中,有以下与使用select()等价的写法:

c
ready = pselect(nfds, &readfds, &writefds, &exceptfds, timeout, &sigmask);

/* 相当于下边的写法 */
sigset_t origmask;
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);

首先,我们在前边的学习中知道在Linux下按下Ctrl+C会产生SIGINT信号,该信号的编号为2。所以我们以该信号为例,下进行一个说明:

(1)我们定义了一个信号集 origmask ,目前还没有向里边添加任何需要被阻塞的信号,这个时候不会有信号被阻塞;

(2)我们将 SIGINT 信号添加到这个信号集中去,这个时候这个信号就会在某种情况下被阻塞,也就是该信号不会被传递给进程;

(3)我们调用了 pselect() 函数,在该函数调用期间,SIGINT 信号将会一直被则色,该信号无法被传递给当前进程;

(4)当pselect()函数返回后,会恢复之前的信号掩码,SIGINT 阻塞状态被取消,此时我们按下 Ctrl + C 便可以正常向进程传递 SIGINT 信号了。

2.3 与select()的不同 ​

使用man手册的时候,我们会看到以下说明:

shell
 The operation of select() and pselect() is identical, other than these three differences:

• select()  uses  a timeout that is a struct timeval (with seconds and microseconds), while pselect() uses a struct timespec (with seconds and nanoseconds).

• select() may update the timeout argument to indicate how much time was left.  pselect() does not change this argument.

• select() has no sigmask argument, and behaves as pselect() called with NULL sigmask.

意思就是,select()和pselect()的操作是相同的,但会有以下三个不同的地方:

  • select()使用timeval结构(带有秒和微秒)的超时,而pselect()使用timepec结构(带有秒和纳秒)。
  • select()可以更新timeout参数来表示剩余时间,而pselect()不会改变这个参数。
  • select()没有sigmask参数,在pselect()调用时,若将sigmask设为NULL,此时这两个函数是等价的。

二、select使用流程图 ​

需要读取数据时,大概的流程如下:

  • (1)把关心的文件描述符添加到文件描述符集合中;
  • (2)调用select()/poll()函数来监控文件描述符集合,然后会阻塞等待集合中一个或者多个文件描述符有数据;
  • (3)当有数据时,函数返回;
  • (4)依次判断哪个文件描述符有数据;
  • (5)依次处理有数据的文件描述符的数据。
image-20220702183118035

三、文件描述符集操作函数 ​

1. 数据类型:fd_set ​

上边,我们介绍了select()函数的各个参数,下边来了解一些这个文件描述符集,也就是这个fd_set类型的数据。这个数据类型定义在sys/select.h文件中,关于这个数据类型的定义如下:

c
/* fd_set for select and pselect.  */
typedef struct
{
	/* XPG4.2 requires this member name.  Otherwise avoid the name
	   from the global namespace.  */
#ifdef __USE_XOPEN
	__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
	__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;

会发现,fd_set是一个结构体,它的成员是一个__fd_mask类型的数组,数组的大小为__FD_SETSIZE / __NFDBITS。

还是在这个文件中,我们会找到如下定义:

c
/* The fd_set member is required to be an array of longs.  */
typedef long int __fd_mask;

/* Maximum number of file descriptors in `fd_set'.  */
#define FD_SETSIZE              __FD_SETSIZE

#ifdef __USE_MISC
/* Sometimes the fd_set member is assumed to have this type.  */
typedef __fd_mask fd_mask;

/* Number of bits per word of `fd_set' (some code assumes this is 32).  */
# define NFDBITS                __NFDBITS
#endif

从这里我们知道__fd_mask实际上为long int类型,在Ubuntu21.04的64位版中,该类型占8个字节。常见有关 int 的数据类型所占字节数的情况如下:

c
                      (short int)    (int)    (long)    (long long)
 8位系统/编译器:
16位系统/编译器:          2            2        4
32位系统/编译器:          2            4        4           8
64位系统/编译器:          2            4        8           8

__FD_SETSIZE / __NFDBITS是多少呢?我自己是没有找到这两个到底是定义在哪里,后边找到了再补充到这里把。

总之来说,fd_set就是一个long类型数组,数组中所有元素按照二进制位排列,每一位都对应一个文件描述符)。大概就是下图这个样子,集合的每一个元素代表了一个文件描述符的状态,准备好了就置1。

image-20220702182401571

在使用select()函数之前,需要先了解一下文件描述符的相关操作,这里一共有四个函数:

c
void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

2. FD_CLR() ​

2.1 函数说明 ​

在linux下可以使用man 2 FD_CLR命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
void FD_CLR(int fd, fd_set *set);

【函数说明】该函数用于从文件描述符集中移除一个文件描述符。

【函数参数】

  • fd:int类型,表示要移除的文件描述符。
  • set:fd_set类型,表示已经定义好的文件描述符集。

【返回值】none

【使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
FD_CLR(fd, &fset);

【注意事项】none

2.2 使用实例 ​

暂无。

3. FD_ISSET() ​

3.1 函数说明 ​

在linux下可以使用man 2 FD_ISSET命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
int  FD_ISSET(int fd, fd_set *set);

【函数说明】该函数用于测试指定的文件描述符是否还存在于指定的文件描述符集。

【函数参数】

  • fd:int类型,表示要测试的文件描述符。
  • set:fd_set类型,表示已经定义好的文件描述符集。

【返回值】int类型,文件描述符fd在set中存在,FD_ISSET()返回非0值,如果不存在则返回0。

【使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
if(FD_ISSET(0, &rset))
{
    ...
}

【注意事项】none

3.2 使用实例 ​

暂无。

4. FD_SET() ​

4.1 函数说明 ​

在linux下可以使用man 2 FD_SET命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
void FD_SET(int fd, fd_set *set);

【函数说明】该函数用于向文件描述符集中添加一个我们关心的文件描述符。

【函数参数】

  • fd:int类型,表示要添加的文件描述符。
  • set:fd_set类型,表示已经定义好的文件描述符集。

【返回值】none

【使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
FD_SET(fd, &fset);

【注意事项】none

4.2 使用实例 ​

暂无。

5. FD_ZERO() ​

5.1 函数说明 ​

在linux下可以使用man 2 FD_ZERO命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/select.h>

/* 函数声明 */
void FD_ZERO(fd_set *set);

【函数说明】该函数用于将一个文件描述符集合初始化为空。

【函数参数】

  • set:fd_set类型,表示需要初始化为空的文件描述符集。

【返回值】none

【使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <sys/select.h>

/* 至少应该有的语句 */
fd_set fset
FD_ZERO(&fset);

【注意事项】none

5.2 使用实例 ​

暂无。

四、select()使用实例 ​

在下边的实例中,我们使用TCP协议的客户端和服务器端进行测试,在下边的测试源码中,我们的客户端和服务器端接收的数据打印的时候都没有换行,其实看前边的几篇笔记中的一些例子就会发现,我们打印出来的数据间隔大部分都是两行左右,间距太大了,这是因为从标准输入的时候将enter也一起读取了,所以发送出去的数据本身就有一个换行符。

1. server服务器端 ​

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: server.c
 * Author   : fanhua
 * Description: server服务器端——select(TCP)
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>      /* perror */
#include <stdlib.h>     /* exit atoi */
#include <errno.h>      /* errno号 */
#include <sys/types.h>  /* socket           bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h>    /* bzero */
#include <arpa/inet.h>  /* htons  inet_addr inet_pton */
#include <netinet/in.h> /* ntohs  inet_addr inet_ntop*/
#include <unistd.h>     /* close */
#include <string.h>     /* strlen strcat*/
#include <sys/select.h> /* select */
void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	/* 1.创建套接字,得到socket描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.将套接字与指定端口号和IP进行绑定 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin));  /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;    /* 协议族, IPv4 */
	sin.sin_port = htons(port);  /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2绑定 */
	if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	/*3.调用listen()把主动套接字变成被动套接字 */
	if (listen(socket_fd, 5) < 0)
	{
		perror("listen");
		exit(-1);
	}
	printf ("Server starting....OK!\n");
	/*4.阻塞等待客户端连接请求相关变量定义 */
	int newfd = -1;
	struct sockaddr_in cin;          /* 用于存储成功连接的客户端的IP信息 */
	socklen_t addrlen = sizeof(cin);
	/* 5.打印成功连接的客户端的信息相关变量定义 */
	char ipv4_addr[16];
	/* 6.数据读写相关变量定义 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	/* 7.select实现多路复用相关变量定义 */
	int i = 0;
	fd_set rset, cpy_rset;   /* 用于检测读状态的文件描述符集 */
	int maxfd = -1;/* 最大文件描述符编号 */
	struct timeval tout;/* 超时时间结构体变量 */
	tout.tv_sec = 5;    /* 超时时间秒数 */
	tout.tv_usec = 0;   /* 超时时间微秒数 */
	FD_ZERO(&rset);     /* 初始化文件描述符集 */
	FD_SET(socket_fd, &rset);
	maxfd = socket_fd;
	/* 8.数据处理 */
	while(1)
	{
		FD_ZERO(&cpy_rset);
		cpy_rset = rset;
		/* 8.1调用select开始检测文件描述符 */
		ret = select(maxfd + 1, &cpy_rset, NULL, NULL, &tout);
		if(ret < 0)
		{
			perror("select");
			continue;
		}
		/* 8.2遍历返回后的文件描述符集 */
		for(i = 0; i < maxfd + 1; i++)
		{
			if(FD_ISSET(i, &cpy_rset))
			{
				/* 有新的连接请求时的处理 */
				if(i == socket_fd)
				{
					if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
					{
						perror("accept");
						exit(-1);
					}
					FD_SET(newfd, &rset);/* 添加到文件描述符集 */
					if(maxfd < newfd) maxfd = newfd;/* 更新maxfd */
					if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
					{
						perror ("inet_ntop");
						exit(-1);
					}
					printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);

				}
				else /* 读取数据并返回 */
				{
					bzero(buf, BUFSIZ);
					bzero(replay, BUFSIZ);
					do{
						ret = read(i, buf, BUFSIZ - 1);
					}while(ret < 0 && EINTR == errno);
					if(ret < 0)
					{
						perror("read");
						exit(-1);
					}
					if(!ret) /* 对方已经关闭 */
					{
						FD_CLR(i, &rset);
						close(i);
						printf("closed client: %d \n", i);
					}
					else
					{
						printf("Receive data[client_fd=%d]: %s", i, buf);
						strcat(replay, buf);
						ret = send(i, replay, strlen(replay), 0);
						if(ret < 0)
						{
							perror("send");
							continue;
						}
					}
				}
			}
		}
	} 
	/* 9.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

2. client客户端 ​

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: cilent.c
 * Author   : fanhua
 * Description: client客户端程序——select(TCP)
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket           connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h>   /* bzero strncasecmp strlen */
#include <sys/select.h> /* select */
void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	int portClient = -1;
	if (argc != 4)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	portClient = atoi(argv[3]);
	if (port < 5000 || portClient < 5000 || (port == portClient))
	{
		usage(argv[0]);
		exit(-1);
	}

	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 绑定固定的IP 和 端口号 */
	struct sockaddr_in sinClient;
	bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
	sinClient.sin_family = AF_INET;   /* 协议族 */
	sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
	{
		perror("bind");
		exit(-1);
	}

	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写相关变量定义 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	/* 4.select实现多路复用相关变量定义 */
	fd_set rset;   /* 用于检测读状态的文件描述符集 */
	int maxfd = -1;/* 最大文件描述符编号 */
	struct timeval tout;/* 超时时间结构体变量 */
	tout.tv_sec = 5;   /* 设置超时时间秒数 */
	tout.tv_usec = 0;  /* 设置超时时间微秒数 */
	/* 5.开始数据传输 */
	while (1)
	{
		FD_ZERO(&rset);    /* 清空文件描述符集 */
		FD_SET(0, &rset);  /* 将文件描述符0(标准输入)添加到文件描述符集 */
		FD_SET(socket_fd, &rset); /* 将用于监听的socket套接字添加到文件描述符集 */
		maxfd = socket_fd;        /* 重置最大文件描述符集大小 */
		/* 5.1开始检测文件描述符 */
		ret = select(maxfd + 1, &rset, NULL, NULL, &tout);
		if(ret < 0)
		{
			perror("select");
			continue;
		}
		/* 5.2标准键盘文件描述符就绪时的处理 */
		if(FD_ISSET(0, &rset))
		{
			bzero(buf, BUFSIZ);/* 清空buf */
			do{
				ret = read(0, buf, BUFSIZ - 1);/* 从标准输入获取数据 */
			}while(ret < 0 && EINTR == errno);
			if (ret < 0)/* 获取数据失败 */
			{
				perror ("read() from stdin");
				continue;
			}
			/* 获取数据成功,但是标准输入中没有数据,不需要写入,继续循环即可 */
			if (!ret) continue;
			/* 向服务器发送数据 */
			if(write(socket_fd, buf, strlen(buf)) < 0)/* 将从标准输入获取的数据写入到socket 网络套接字中 */
			{
				perror ("write to socket error");
				continue;
			}
			/* 判断是否需要退出 */
			if (!strncasecmp(buf, "quit", strlen ("quit")))  	//用户输入了quit字符
			{
				printf ("Client is exiting!\n");
				break;
			}
			
		}
		/* 5.3socket文件描述符就绪时的处理(服务器有数据发送过来) */
		if(FD_ISSET(socket_fd, &rset))
		{
			bzero (replay, BUFSIZ);
			do{
				ret = recv(socket_fd, replay, BUFSIZ, 0);
			}while (ret < 0 && EINTR == errno);
			if(ret < 0)
			{
				perror("recv");
				continue;
			}
			/* 若服务器已关闭,则直接退出客户端 */
			if (ret == 0) break;
			printf("server replay:%s", replay);
			if (!strncasecmp(buf, "quit", strlen("quit")))
			{
				printf ("Sender Client is exiting... ...!\n");
				break;
			}
		}
	}
	/* 6.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

3. Makefile ​

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

makefile
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author   : fanhua
# Description: Makefile文件
## ======================================================
# 
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} 
all : $(TARGET_LIST)

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
	@rm -vf $(TARGET_LIST) 
	
clean_o :
	@rm -vf *.o
	
clean_out :
	@rm -vf *.out

4. 测试结果 ​

我们执行以下命令编译链接程序,生成两个可执行文件:

shell
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client:

shell
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall    server.c   -o server

对于服务器端,我们执行以下命令启动服务器进程:

shell
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

shell
./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003

然后我们就会看到如下现象:

image-20220703112747780

可以发现我们发送的数据,在客户端都被打印了出来,说明客户端与服务器端可以进行正常通信。