Linux网络编程

目录

一、网络协议模型

OSI七层模型

TCP/UDP模型

二、IP地址

三、字节序

四、TCP CS模型的搭建

服务器分类

1、重复型(循环型)

2、并发型

常用函数接口介绍

1、socket

2、bind

3、listen

4、accept

5、send

6、recv

7、connect

服务器与客户端搭建的基本流程

1、TCP服务器的搭建流程及代码

2、TCP客户端的搭建流程及代码

五、循环服务器的搭建

 1、阻塞的循环服务器

详细代码展示

处理效果展示

结果分析

2、非阻塞的循环服务器(利用fcntl函数)

实现原理

详细代码展示

运行效果展示

结果分析 

六、并发服务器的搭建

1、多进程并发服务器

实现原理

详细代码展示

运行效果展示

结果分析

2、多线程并发服务器

详细代码展示

运行效果展示

3、IO多路复用的并发服务器(利用select函数)

select函数详解

实现原理

服务器基本框架

详细代码展示(带详细注释)

运行结果展示


一、网络协议模型

OSI七层模型

OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

应用层:提供应用服务

表示层:数据的表示和加密·

会话层:建立新会话

传输层:约束传输方式

网络层:实现跨子网通信

数据链路层:实现同一子网内数据交换

物理层:约束一些物理标准

其中,路由器工作在OSI七层协议模型的第三层,即网络层(网际层)的设备,主要用于组网(跨子网通信)、对数据进行转发(路由)、维护路由表。

OSI 只是存在于概念和理论上的一种模型,它的缺点是分层太多,增加了网络工作的复杂性,没有大规模应用。后来人们对 OSI 进行了简化,合并了一些层,从下到上分别是接口层、网络层、传输层和应用层,这就是TCP/IP 模型。 

在TCP/IP模型中,网络层和运输层之间的区别是最为关键的:网络层( IP)提供点到点的服务,而运输层(TCP和UDP)提供端到端的服务

TCP、UDP协议属于传输层、IP协议属于网络层(互联网层)

TCP/UDP模型

TCP(传输控制协议)和UDP(用户数据报协议)

TCP协议是一种面向连接的协议,可靠性高,能丢包重发,有丰富的校验机制;

UDP协议是一种面向报文的协议,可靠性差,需依靠应用层提高可靠性。

二、IP地址

IP地址的结构: 网络号 + 主机号 ,根据结构不同分为ABCDE五类。

IP地址需和子网掩码配套使用,单独说IP地址没有任何意义。

网络号相同的IP,说明在同一个子网内。

子网掩码是用来划分子网的,是一个32bit的二进制数,用1来掩网络号,用0来掩主机号。

三、字节序

在进行网络通信时,需要注意字节序不同的问题

大端字节序-------大端序---------网络字节序

小段字节序-------小端序---------主机字节序

小端序:低字节存低位

大端序:低字节存高位

ps:检测自己主机的字节序,可以通过一个共用体,内部定义一个数据和一个数组的方式来检查。

四、TCP CS模型的搭建

服务器分类

1、重复型(循环型)

①等待一个客户端请求的到来

②处理客户端请求

③发送响应给发送请求的客户端

④重新回到步骤①

2、并发型

①等待一个客户端请求的到来

②启动一个新服务器来处理这个客户端请求。在这个新服务器中可能开辟新的进程、线程,生成新的任务,具体如何操作则依赖于底层操作系统,在对客户端服务完毕后,这个新开启的服务端关闭

③重复进行①的操作

总结:在并发型服务器的架构下,每个客户端都有一个与之对应的服务端,也就可以同时为多个客户端服务。

常用函数接口介绍

1、socket

目的:创建一个通信端口

int socket(int domain, int type, int protocol)

参数domain:指定所使用的协议族,像IPV4、IPV6协议族

参数type:套接字的类型,像流式套接字、数据报套接字

参数protocol:协议,一般是给0,表示默认


2、bind

目的:给socket绑定IP和端口

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen)

参数sockfd:有socket创建的文件描述符,即待绑定的socket

参数sockaddr *addr:这是一个结构体首地址,里面存储着ip号和端口的首地址

参数socklen_t addrlen:参数二指向的结构体的大小


需要注意的是bind函数的第二个参数所用的类型是struct sockaddr,而Linux为TCP通信所提供的结构体类型是struct sockaddr_in,因此在作为bind函数的入参时,需要进行一次数据类型的强制转换。这一点在后面的实际程序代码实现中会有体现。


3、listen

目的:可理解为创建了一个监听队列,来标记客户端的连接请求,如果队列已经满了。则客户端会收到连接被拒的错误。

int listen(int sockfd, int backlog)

参数sockfd:监听套接字的文件描述符,经由socket创建,bind绑定后的那个套接字

参数backlog:监听队列的大小

listen函数一般用于bind绑定之后,accept监听之前


4、accept

目的:接收一个套接字的连接请求

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数sockfd:由listen监听过后产生的监听套接字的文件描述符

参数addr:指向一个结构体的指针,结构体用于存储对端的数据

参数addrlen:参数2所指向的结构体的字节大小长度

返回值:成功返回通信套接字的文件描述符,失败返回-1和错误码


5、send

目的:发送数据,通过socket

ssize_t send(int sockfd, const void *buf, size_t len, int flags)

参数sockfd:要进行通信的通信套接字的文件描述

参数buf:要进行发送的数据的首地址

参数len:要发送的数据的长度

参数flags:默认给0


6、recv

目的:接收数据,通过socket

ssize_t recv(int sockfd, void *buf, size_t len, int flags)

参数socketfd:要进行通信的通信套接字文件描述符

参数buf:用于存储接收到的数据的内存首地址

参数len:要接收的数据长度(字节数)

参数flags:默认给0

返回值:成功返回成功接收到的字节数,失败返回-1和错误码,0:表示对端执行了一个有序关闭。

7、connect

目的:发起一个socket的连接请求

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数sockfd:客户端socket函数创建的套接字文件描述符。

参数addr:这是一个结构体首地址,里面存储着服务器的ip号和端口的首地址。

参数addrlen:参数2指向的结构体所占的内存空间大小。


服务器与客户端搭建的基本流程

1、TCP服务器的搭建流程及代码

socket -> bind -> listen -> accept -> send/recv -> close

2、TCP客户端的搭建流程及代码

socket -> connect -> send/recv -> close

客户端其实并没有什么值得深究的,此处给出的客户端代码是通用的,在文章后面的与服务端之间的通信实验中,都采用的是这个客户端程序。

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>

int main()
{
	//socket
	int clifd = -1;
	clifd = socket(AF_INET, SOCK_STREAM, 0);
	if(clifd < 0)
	{
		puts("socket error.");
		return -1;
	}
	puts("socket success.");
	//connect
	struct sockaddr_in myser;
	myser.sin_family = AF_INET;//采用ipv4机制
	myser.sin_port = htons(7777);//端口号设为7777
	myser.sin_addr.s_addr = inet_addr("127.0.0.1");//采用本地回环地址

	int ret = connect(clifd, (struct sockaddr *)&myser, sizeof(myser));
	if(ret != 0)
	{
		puts("connect error.");
		close(clifd);
		return -1;
	}
	puts("connect success.");
	//send or recv
	char buf[50];
	memset(buf, 0, sizeof(buf));
	gets(buf);
	send(clifd, buf, strlen(buf), 0);

	memset(buf, 0, sizeof(buf));
	ret = recv(clifd, buf, sizeof(buf), 0);
	if(ret > 0)
	{
		puts(buf);
	}
	//close
	close(clifd);
	return 0;
}

五、循环服务器的搭建

循环服务器指的是,一个服务器通过分时,处理多个客户端,只有当前一个客户端处理完毕,下一个客户端才开始处理。

 1、阻塞的循环服务器

//伪代码
socket     //创建监听套接字
bind       //绑定IP和端口号
listen     //开启监听
while(1) 
{ 
    connfd = accept     //接受客户端请求
    do_work             //执行相关操作
    close connfd        //关闭客户端通信套节字
}
close listenfd        //关闭服务端监听套接字

详细代码展示

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>

int accept_cli(int listenfd);
int do_work(int connfd);

int main()
{
  int fd = -1;
  //创建一个套接字
  fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0)
	{
    puts("socket fail");
		return -1;
	}
  puts("socket success");
  
  
  int ret = -1;
	struct sockaddr_in myser;  //现有的结构体类型是这个,后面需要进行一次强制的类型转换
	myser.sin_family = AF_INET;
	myser.sin_port = htons(7777);//设置端口号,5000以上可以随意设置,5000以下的被使用了
  myser.sin_addr.s_addr = htonl(INADDR_ANY);
  //给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
	ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
	if(ret != 0)
	{
		puts("bind fail");
    close(fd);
		fd = -1;
		return -1;
	}
  puts("bind success");
 
 
  //进行监听,最大可监听5个
 	ret = listen(fd, 5);
	if(ret != 0)
	{
    puts("listen fail");
		close(fd);
		fd = -1;
		return -1;
	}
  puts("listen start");


  while(1)
  {
    int connfd = accept_cli(fd);
		if(connfd < 0)
		{
            puts("no client connection");
			continue;
		}
		do_work(connfd);
		close(connfd);//关闭通信套接字,因为与其的服务已经结束完成
  }
  
  close(fd);
 
}

//自定义函数部分

int accept_cli(int listenfd)
{
	if(listenfd < 0)
	{
		puts("lisenfd < 0");
		return -1;
	}

	int connfd = -1;
	struct sockaddr_in mycli;
	int len = sizeof(mycli);
	connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
	return connfd;
}

int do_work(int connfd)
{
	if(connfd < 0)
	{
		puts("connfd < 0, cannot do work.");
		return -1;
	}
	
	char buf[50];
	memset(buf, 0, sizeof(buf));
	int ret = recv(connfd, buf, sizeof(buf), 0);
	if(ret > 0)
	{
		puts(buf);
		send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
	}
	else if(ret == 0)
	{
		return -1;
	}
	return 0;
}

处理效果展示

结果分析

可以看出,阻塞的循环服务器,在处理客户端的任务时。

其循环特性体现在,遵循一个先连接先处理的原则,只有当前一个处理好了才会进行下一个的处理。

其阻塞特性体现在,服务端在没有收到客户端的回应时,其不进行结束,而是一直等待。

2、非阻塞的循环服务器(利用fcntl函数)

//伪代码
socket     //创建监听套接字
bind       //绑定IP和端口号
listen     //开启监听
//设置监听套接字为非阻塞
int flags = fcntl(listenfd, F_GETFL)
fcntl(listenfd, F_SETFL, flags | O_NONBLOCK)
while(1) 
{ 
    connfd = accept     //接受客户端请求
    do_work             //执行相关操作
    close connfd        //关闭客户端通信套节字
}
close listenfd        //关闭服务端监听套接字

由于,在进行读写操作时,recv()、send()、accept()等函数都是阻塞进行的,如果所需的资源没有准备好,他们将会一直等待,直到资源被准备好。

所幸,fcntl()函数可以获得和改变已经打开的文件的性质,通过fcntl()函数可以设置文件的属性为非阻塞。

注:Linux下的socket是一种特殊的文件,其也可以用fcntl()函数进行操作

实现原理

将服务端的监听套接字设置成非阻塞的,通过其循环不断地监听客户端的通信套接字,读取不到,就直接下一个,不做停留,也就不会阻塞,直到有一个通信套接字被读取成功,才进行通信,否则其一直在监听通信套接字。

详细代码展示

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>

int accept_cli(int listenfd);
int do_work(int connfd);

int main()
{
    int fd = -1;
  //创建一个套接字
    fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0)
	{
        puts("socket fail");
		return -1;
	}
    puts("socket success");
  
  
    int ret = -1;
    struct sockaddr_in myser;  //现有的结构体类型是这个,后面需要进行一次强制的类型转换
    myser.sin_family = AF_INET;
    myser.sin_port = htons(8888);//设置端口号,5000以上可以随意设置,5000以下的被使用了
    myser.sin_addr.s_addr = htonl(INADDR_ANY);
  //给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
	ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
	if(ret != 0)
	{
		puts("bind fail");
    close(fd);
		fd = -1;
		return -1;
	}
    puts("bind success");
 
 
  //进行监听,最大可监听5个
 	ret = listen(fd, 5);
	if(ret != 0)
	{
    puts("listen fail");
		close(fd);
		fd = -1;
		return -1;
	}
    puts("listen start");
	
  //改变监听套接字的属性为非阻塞,之一步是搭建非阻塞循环服务器的关键
    int flags = fcntl(fd, F_GETFL);
	fcntl(fd, F_SETFL, flags | O_NONBLOCK);


  while(1)
  {
        int connfd = accept_cli(fd);
		if(connfd < 0)
		{
			puts("no client connection");
			sleep(1);
			continue;
		}
		do_work(connfd);
		close(connfd);//关闭通信套接字,因为与其的服务已经结束完成
  }
  
  close(fd);
 
}

//自定义函数部分

int accept_cli(int listenfd)
{
	if(listenfd < 0)
	{
		puts("lisenfd < 0");
		return -1;
	}

	int connfd = -1;
	struct sockaddr_in mycli;
	int len = sizeof(mycli);
	connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
	return connfd;
}

int do_work(int connfd)
{
	if(connfd < 0)
	{
		puts("connfd < 0, cannot do work.");
		return -1;
	}
	
	char buf[50];
	memset(buf, 0, sizeof(buf));
	int ret = recv(connfd, buf, sizeof(buf), 0);
	if(ret > 0)
	{
		puts(buf);
		send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
	}
	else if(ret == 0)
	{
		return -1;
	}
	return 0;
}

其中,在为socket套接字绑定IP时,用到INADDR_ANY,INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。

当然,不用INADDR_ANY也可以,可以通过直接指定的IP来给IP值

运行效果展示

结果分析 

通过代码,可以看到的是,我们是在服务端accept之前设置socket的属性为非阻塞。

也就是说,在执行accept函数时,将不会再阻塞,一旦没有接收到来自客户端的连接请求,accept函数就会返回,并进行下一次的accept。

通过运行结果,可以看到的是,在没有客户端进行连接的时候,accept函数直接就进行了返回,而阻塞服务器那,accept函数一直在那等着,也就是我们所说的阻塞。

阻塞:函数在没有得到结果前,不会返回,而是会一直等待,直到接收到结果。

非阻塞:函数如果没有得到结果,直接返回,不会一直等待。

六、并发服务器的搭建

循环服务器是分时共享服务器资源,而并发服务器可以在同一时刻,让多个客户端访问。

1、多进程并发服务器

//伪代码
socket 
bind 
listen 
while(1) 
{ 
    connfd = accept() 
    pid_t pid = fork(); 
    if(pid == 0) 
    { 
      do_work() 
      close(connfd) 
      exit() 
    } 
}
close listenfd

实现原理

服务端在接受一个客户端的请求后,通过开辟一个子进程去处理这个客户端的相关事宜,而在主进程中继续保持对客户端的监听。

详细代码展示

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int accept_cli(int listenfd);
int do_work(int connfd);

int main()
{
  int fd = -1;
  //创建一个套接字
  fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0)
	{
    puts("socket fail");
		return -1;
	}
  puts("socket success");
  
  
  int ret = -1;
	struct sockaddr_in myser;  //现有的结构体类型是这个,后面需要进行一次强制的类型转换
	myser.sin_family = AF_INET;
	myser.sin_port = htons(7777);//设置端口号,5000以上可以随意设置,5000以下的被使用了
  myser.sin_addr.s_addr = htonl(INADDR_ANY);
  //给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
	ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
	if(ret != 0)
	{
		puts("bind fail");
    close(fd);
		fd = -1;
		return -1;
	}
  puts("bind success");
 
 
  //进行监听,最大可监听5个
 	ret = listen(fd, 5);
	if(ret != 0)
	{
    puts("listen fail");
		close(fd);
		fd = -1;
		return -1;
	}
  puts("listen start");
	

  while(1)
  {
		int connfd = accept_cli(fd);
		if(connfd < 0)
		{
			continue;
		}
		
    pid_t pid = fork();  //开辟子进程进行对客户端的具体处理
		
    if(pid == 0)  //等于0的是子进程
		{
			do_work(connfd);
			close(connfd);
			exit(0);    //处理完后子进程退出
		}
  }
  
  close(fd);
 
}

//自定义函数部分

int accept_cli(int listenfd)
{
	if(listenfd < 0)
	{
		puts("lisenfd < 0");
		return -1;
	}

	int connfd = -1;
	struct sockaddr_in mycli;
	int len = sizeof(mycli);
	connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
	return connfd;
}

int do_work(int connfd)
{
	if(connfd < 0)
	{
		puts("connfd < 0, cannot do work.");
		return -1;
	}
	
	char buf[50];
	memset(buf, 0, sizeof(buf));
	int ret = recv(connfd, buf, sizeof(buf), 0);
	if(ret > 0)
	{
		puts(buf);
		send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
	}
	else if(ret == 0)
	{
		return -1;
	}
	return 0;
}

运行效果展示

结果分析

根据实验结果,可见后连接的也可以先进行通信,这就是高并发,展现出来的效果不同于阻塞的先后顺序处理。

2、多线程并发服务器

//伪代码
void *pthread_do_work(void *fd) 
{ 
    int connfd = (int)fd; 
    if(connfd < 0) 
    { 
        return NULL; 
    }
    do_work(connfd); 
    return NULL; 
}
socket 
bind 
listen 
while(1) 
{ 
    connfd = accept()
    pthread_create(&tid,NULL,pthread_do_work,(void *)connfd); 
}
close listenfd

注意:

1、自定义的线程处理函数的后面不要有阻塞,否则会导致无法实现并发。

 2、线程处理函数之后,不要写关闭通信套接字的语句,否则可能会导致,套接字已经被关闭,而无法被线程处理函数使用。

详细代码展示

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>

int do_work(int connfd);
int accept_cli(int listenfd);

//子线程处理函数
void *pth_func_cli(void *fd)
{
	int connfd = (int)fd;
	if(connfd < 0)
	{
		return NULL;
	}
	printf("connfd:%d\n",connfd);
	puts("do work");
	do_work(connfd);
	return NULL;
}


int main()
{
  int fd = -1;
  //创建一个套接字
  fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0)
	{
    puts("socket fail");
		return -1;
	}
  puts("socket success");
  
  
  int ret = -1;
	struct sockaddr_in myser;  //现有的结构体类型是这个,后面需要进行一次强制的类型转换
	myser.sin_family = AF_INET;
	myser.sin_port = htons(7777);//设置端口号,5000以上可以随意设置,5000以下的被使用了
  myser.sin_addr.s_addr = htonl(INADDR_ANY);
  //给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
	ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
	if(ret != 0)
	{
		puts("bind fail");
    close(fd);
		fd = -1;
		return -1;
	}
  puts("bind success");
 
 
  //进行监听,最大可监听5个
 	ret = listen(fd, 5);
	if(ret != 0)
	{
    puts("listen fail");
		close(fd);
		fd = -1;
		return -1;
	}
  puts("listen start");
	

  while(1)
	{
		//accept_cli
		int connfd = accept_cli(fd);
		if(connfd < 0)
		{
			continue;
		}
		else
		{
			puts("accept success.");
			
      pthread_t tid;
			int ret = pthread_create(&tid, NULL, pth_func_cli,(void *) connfd);  //创建线程
			
      if(ret != 0)
			{
				puts("pthread create error.");
				close(connfd);
				continue;
			}
		}
	}
	close(fd);
	return 0;
 
}

//自定义函数部分


int accept_cli(int listenfd)
{
	if(listenfd < 0)
	{
		puts("lisenfd < 0");
		return -1;
	}
	int connfd = -1;
	struct sockaddr_in mycli;
	int len = sizeof(mycli);
	connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
	return connfd;
}

int do_work(int connfd)
{
	if(connfd < 0)
	{
		puts("connfd < 0, cannot do work.");
		return -1;
	}
	
	char buf[50];
	memset(buf, 0, sizeof(buf));
	int ret = recv(connfd, buf, sizeof(buf), 0);
	if(ret > 0)
	{
		puts(buf);
		send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
	}
	else if(ret == 0)
	{
		return -1;
	}
	return 0;
}

运行效果展示

多线程的服务器显现出来的效果上与多进程并无区别,区别在于多线程技术所用开销要比多进程技术小。

3、IO多路复用的并发服务器(利用select函数)

首先,Linux提供了三种IO多路复用的接口,分别是select、poll、epoll,在这里我们对select函数做详细展开。

select函数详解

函数形式

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

函数功能

实现对想要监听的文件描述符集合的监听,一旦有一个或者多个文件描述符进入IO就绪态时,select函数就会返回,执行相应的IO操作,否则,select函数会一直阻塞在那,并由内核(kernel)轮询地查看所有的文件描述符状态。

函数参数详解

参数nfds:最大fd+1,在三个描述符集合中找到最大的描述符+1

参数readfds:读文件描述符集合的首地址

参数writefds:写文件描述符集合的首地址

参数exceptfds:监听其他操作的文件描述符集合的首地址

参数timeout:一般给null,表示设置成阻塞,如果不想阻塞,可以设置struct timeval这个结构体的值,然后赋给timeout,表示要等待的秒数和微秒数

struct timeval { 
    __time_t tv_sec;            /* Seconds. */
    __suseconds_t tv_usec;     /* Microseconds. */ 
};

返回值:成功,则返回所有准备好的文件描述符的个数,失败时,返回0表示超时,返回-1表示错误码。

select函数在对文件描述符进行操作时,主要用到以下这几个宏函数

宏函数功能
FD_ZERO(fd_set *set)清空一个文件描述符集合
FD_CLR(int fd,fd_set *set)从文件描述符集合中清除一个文件描述符
FD_SET(int fd, fd_set *set)向文件描述符集合中添加一个文件描述符
FD_ISSET(int fd,fd_set *set)测试指定的文件描述符是否在该集合中
fd_set在后续程序中也有使用到,fd_set 本质上是个数组: long [32],是有系统给我们封装好的。此外,还定义了一个宏#define __FD_SETSIZE 1024,单进程中Linux最大的文件文件描述符监视数量为1024,这个值在内核中已经被定义,如需扩大,需重新编译内核。

实现原理

服务器基本框架

//伪代码
socket 
bind 
listen 
准备读的文件描述符集合 
清空读的文件描述符集合 
将监听套接字加入读文件描述符集合中
while(1)
{
    因为select会修改文件描述符集合,一旦修改,我们就不知道该监听谁了,因此,我们进入操作前要先备份
    fd_set tempfds = readfds;
    int fdnum = select(FD_SETSIZE, &tempfds, NULL, NULL, NULL);
    if(fdunm > 0)
    {
        int i = 0;
        for(i = 0;i < FD_SETSIZE;i++)
        {
            int connfd = accept();
            if(connfd > 0)
            {
                将新接收到的通信套接字加入到文件描述符集合
                FD_SET(connfd, &readfds);
            }
            else
            {
                continue;
            }
        }
        else
        {
            do_work()
            if客户端已关闭,recv的返回值为0时
            {
                FD_CLR(i, &readfds);
                close(i);
            }
        }
    }
    
}
close(listenfd)

详细代码展示(带详细注释)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>

int main()
{
	//socket
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(listenfd < 0)
	{
		puts("socket error.");
		return -1;
	}
	puts("socket success.");

	struct sockaddr_in myser;
	int ret = -1;
	myser.sin_family = AF_INET;
	myser.sin_port = htons(7777);
	myser.sin_addr.s_addr = htonl(INADDR_ANY);
    //myser.sin_addr.s_addr = inet_addr("192.168.1.1");或者像这样写也可以的

	ret = bind(listenfd, (struct sockaddr *)&myser, sizeof(myser));
	if(ret != 0)
	{
		puts("bind error.");
		close(listenfd);
		return -1;
	}
	puts("bind success.");
	
	ret = listen(listenfd, 5);
	if(ret != 0)
	{
		puts("listen error.");
		close(listenfd);
		return -1;
	}
	puts("listen success.");
  
    //在此之前的都是服务器端初始化的基本必要操作

	fd_set readfds,tempfds;   //fd_set 本质上是个数组: long [32] 
   //此步创建了两个文件描述符集合,一个用来存放读文件描述符,一个用作临时变量
	
    FD_ZERO(&readfds);    //清空读文件描述符集合
	FD_SET(listenfd, &readfds);//向读文件描述符集合中写入我们前边准备好的监听套接字

	while(1)
	{
		tempfds = readfds;    //赋值给另一个变量,自己保持不变

		ret = select(FD_SETSIZE, &tempfds, NULL, NULL, NULL);
        //select函数的调用,本次服务器搭建的关键
        //FD_SETSIZE限制了文件描述符的最大值,最大值也就是个数
        //&tempfds打开的是对读文件描述符集合的监听,其他几个都关闭
        //最后一个参数给null表示设置select为阻塞

		if(ret > 0)    //ret大于0表示有文件描述符已经准备就绪,可以开始操作
		{
			int i = 0;
			for(i = 0; i < FD_SETSIZE; i++)    //遍历所有文件描述符,也就是所有文件来一遍
			{
				if(FD_ISSET(i, &tempfds))//判断,只对在读文件描述符集合中的文件进行进一步操作 
				{
					if(i == listenfd)        //如果是监听套接字
					{
						//accept
						int connfd = -1;
						struct sockaddr_in mycli;
						int len = sizeof(mycli);
						connfd = accept(i, (struct sockaddr *)&mycli, &len);//connfd返回的是通信套接字
						
                        if(connfd > 0)    //如果服务器受理成功
						{
							FD_SET(connfd, &readfds); //将客户端的通信套接字,放入读文件描述符集合
						}
						else
						{
							continue;        //没成功就继续
						}
					}
					else    //如果不是监听套接字,那就是通信套接字
					{
						//send or recv
						char buf[50] = {0};
						memset(buf, 0, 50);
						ret = recv(i, buf, 50, 0);  //从通信套接字中获取数据,并放入buf存储
						if(ret > 0)    //读取成功则输出
						{
							puts(buf);
							send(i, buf, 50, 0);    //并将数据再发回客户端
						}
						else if(ret == 0)   //recv的返回值是0表示,客户端已经关闭         
						{
							FD_CLR(i, &readfds); //将此客户端的通信套接字从读文件描述符集合中移除
							close(i);//关闭这个文件
						}
					}
				}
			}
		}
	}
	close(listenfd);
	return 0;
}

运行结果展示

    

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翔在天上飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值