Linux 网络编程

网络编程大体内容
IO进程 - 本地进程通信
两台不同主机如何不借助第三方软件通信 - 网络 linux下的网络编程
文件IO 标准IO - 文件(Linux下一切皆是文件,设备(字符设备、块设备))
特殊设备

文章目录

一、网络发展史

【腾讯文档】internet历史

二、局域网和广域网

局域网(LAN)
局域网的缩写是LAN,local area network,顾名思义,是个本地的网络,只能实现小范围短距离的网络通信。我们的家庭网络是典型的局域网。电脑、手机、电视、智能音箱、智能插座都连在路由器上,可以互相通信。局域网,就像是小区里的道路,分支多,连接了很多栋楼。
在这里插入图片描述
广域网(Wan)
广域网(Wide Area Network)是相对局域网来讲的,局域网的传输距离比较近,只能是一个小范围的。如果需要长距离的传输,比如某大型企业,总部在北京,分公司在长沙,局域网是无法架设的。广域网,就像是大马路,分支可能少,但类型多,像国道、省道、高速、小道等,连接了很多大的局域网。
在这里插入图片描述

2.1 光猫

光猫是一种类似于基带modem(数字调制解调器)的设备,和基带modem不同的是接入的是光纤专线,是光信号。用于广域网中光电信号的转换和接口协议的转换,接入路由器,是广域网接入。
在这里插入图片描述
将光线插入左侧的灰色口,右侧网口接网线到路由器即可。

2.2 交换机和路由器

交换机(二层):用于局域网内网的数据转发路由器(三层):用于连接局域网和外网
路由器有交换机的功能,反之不成立,交换机没有IP分配和IP寻址的功能。
交换机各个口是平等的,所有接入的设备需要自己配置IP,然后组成局域网。
在这里插入图片描述
路由器需要区分WAN口和LAN口,WAN口是接外网的(从Modem出来的或者从上一级路由器出来的),LAN口是接内网的,现在路由器都带无线功能,本质上无线接入就是LAN。
在这里插入图片描述

2.3 网线

背过一种线序,了解网线的制作流程。
网线线序
网线制作方法

三、IP地址

3.1 基本概念

●IP地址是Internet中主机的标识
● Internet中的主机要与别的机器通信必须具有一个IP地址
● IP地址为32位(IPv4)或者128位(IPv6)
● 表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。
ipv4 五类:A B C D E

3.2 ip地址划分(IPv4)

二级划分 ip=网络号+主机号
网络号:表示是否在一个网段内(局域网)
主机号:标识在本网段内的ID,同一局域网不能重复
在这里插入图片描述
ip地址取值范围:

A类:1.0.0.1~126.255.255.254
B类:128.0.0.1~~191.255.255.254
C类:192.0.0.1~~223.255.255.254   
D类(组播地址):224.0.0.1~~239.255.255.254
E类:保留待用   11110

在这里插入图片描述

3.3 特殊地址

0.0.0.0:在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。
127.0.0.1:回环地址/环路地址,所有发往该类地址的数据包都应该被loop back。
每一个网段主机号为0的地址是网络地址,设置网关主机号为1的地址,主机号最大的地址是该网段的广播地址。全网广播地址255.255.255.255

3.4 子网掩码

● 子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址;
● 子网掩码长度是和IP地址长度完全一样;
网络号全为1,主机号全为0;
255.0.0.0 - A
255.255.0.0 - B
255.255.255.0 - C -
子网掩码 & ip地址 = 网络地址 (网段)
~子网掩码 & ip地址 = 主机地址

3.5 子网号概念

三级地址 ip=网络号+子网号+主机号
网络号+子网号 -网段(网络地址)
练习1:

某公司有四个部门:行政、研发1、研发2、营销,每个部门各30台计算机接入公司局域网交换机,如果要在192.168.1.0网段为每个部门划分子网,子网掩码应该怎么设置,每个子网的地址范围分别是什么?(4个部门之间不能通信) 256 - 4 = 64 => 2^6
192.168.1.0 - 254
子网掩码:255.255.255.1100 0000 - 主机号:0000 0000 - 1111 1111
00 0000 - 11 1111 》64
划分后: 子网掩码-》 255.255.255.192 将主机号划分两位作为网络号:取值-00 01 10 11
192.168.1. 00 网段 000000 - 111111
192.168.1.0 ~ 192.168.1.63
192.168.1.0网络地址 , 192.168.1.63广播地址


192.168.1. 01 网段
192.168.1. 64 ~ 192.168.1.127
192.168.1.64网络地址 , 192.168.1.127广播地址


192.168.1. 10 网段
192.168.1. 128 ~ 192.168.1. 191
192.168.1.128网络地址 , 192.168.1.191广播地址


192.168.1. 11 网段
192.168.1. 192~192.168.1. 255
192.168.1.192网络地址 , 192.168.1.255广播地址
划分为8个网段:每个网段链接30台
192.168.1.0 - 254
子网掩码:255.255.255.1110 0000 - 主机号:0000 0000 - 1111 1111
0 0000 - 1 1111 - >30
000 - 111 000 001 010 011 100 101 110 111

在这里插入图片描述

四、网络模型

4.1 网络模型

undefined 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
undefined 每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务
undefined 网络体系结构即指网络的层次结构和每层所使用协议的集合
undefined 两类非常重要的体系结构:OSI与TCP/IP

4.2 OSI模型

undefined OSI模型是一个理想化的模型,尚未有完整的实现
undefined OSI模型共有七层
undefined OSI现阶段只用作教学和理论研究
在这里插入图片描述

OSI模型
OSI模型是最理想的模型
物理层:传输的是bit流(01一样的数据),物理信号,没有格式
链路层:格式变为帧(把数据分成包,一帧一帧的数据进行发送)
网络层:路由器中是有算法的,ip,(主机到主机)(路由的转发)
传输层:端口号,数据传输到具体那个进程程序(端到端)
会话层:通信管理,负责建立或者断开通信连接
表示层:确保一个系统应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据加密,解密;
应用层:指定特定应用的协议,文件传输,文件管理,电子邮件等。

4.3 TCP/IP模型

在这里插入图片描述
TCP/IP参考模型
在这里插入图片描述
网络接口和物理层:屏蔽硬件差异(驱动),向上层提供统一的操作接口。
**网络层:**提供端对端的传输,可以理解为通过IP寻址机器。
**传输层:**决定数据交给机器的哪个任务(进程)去处理,通过端口寻址
应用层:应用协议和应用程序的集合
OSI和TCP/IP模型对应关系图
在这里插入图片描述
OSI参考模型
在这里插入图片描述

4.4 常见网络协议

网络接口和物理层:
	ppp:拨号协议(老式电话线上网方式)
	ARP:地址解析协议  IP-->MAC
	RARP:反向地址转换协议 MAC-->IP
网络层:	
	IP(IPV4/IPV6):网间互连的协议
	ICMP:网络控制管理协议,ping命令使用
	IGMP:网络分组管理协议,广播和组播使用
传输层:
	TCP:传输控制协议
	UDP:用户数据报协议
应用层:
	SSH:加密协议
	telnet:远程登录协议
	FTP:文件传输协议
	HTTP:超文本传输协议
	DNS:地址解析协议
	SMTP/POP3:邮件传输协议

注意:TCP和IP是属于不同协议栈层的,只是这两个协议属于协议族里最重要的协议,所以协议栈或者模型以之命名了。

五、TCP与UDP

UDP TCP 协议相同点:都存在于传输层
TCP(即传输控制协议):

是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、
数据无失序、数据无重复到达的通信)

适用情况:
 1、适合于对传输质量要求较高,以及传输大量数据的通信。
 2、在需要可靠数据传输的场合,通常使用TCP协议
 3、MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

UDP (用户数据报协议):

UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

适用情况:
1、发送小尺寸数据(如对DNS服务器进行IP地址查询时)
2、在接收到数据,给出应答较困难的网络中使用UDP。
3、适合于广播/组播式通信中。
4、MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
5、流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输

六、socket简介

11982 - Berkeley Software Distributions 操作系统引入了socket作为本地进程之间通信的接口 
21986 - Berkeley 扩展了socket 接口,使之支持UNIX 下的TCP/IP 通信
3》现在很多应用 (FTP, Telnet) 都依赖这一接口 

Socket
 1、是一个编程接口
 2、是一种特殊的文件描述符 (everything in Unix is a file)
 3、并不仅限于TCP/IP协议
 4、面向连接 (Transmission Control Protocol - TCP/IP)
 5、无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)
 
为什么需要Socket?
  普通的I/O操作过程 
  •打开文件->读/写操作->关闭文件 
  •TCP/IP协议被集成到操作系统的内核中,引入了新型的“I/O”操作 
  •进行网络通信的两个进程在不同的机器上,如何连接?
  •网络协议具有多样性,如何进行统一的操作 
需要一种通用的网络编程接口:Socket

6.1 socket类型

流式套接字(SOCK_STREAM) TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字(SOCK_DGRAM) UDP
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问。

6.2 socket的位置

在这里插入图片描述
套接字就是网络进程的ID,可以认为套接字=网络地址ip+端口号。

七、端口号

● 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区分
● TCP端口号与UDP端口号独立
● 端口号一般由IANA (Internet Assigned Numbers Authority) 管理
● 端口用两个字节来表示 2byte

八、字节序

小端序(little-endian) - 低序字节存储在低地址 (主机字节序)
大端序(big-endian)- 高序字节存储在低地址 (网络字节序)
网络中传输的数据必须使用网络字节序,即大端字节序
笔试题:写一个函数,判断当前主机的字节序?
测试方式:共用体、指针强转、数据类型强转
在这里插入图片描述

#include <stdio.h>

union un {
    int a;
    short b;
    char c;
};

int main(int argc, char const *argv[])
{
    union un st;
    st.a=0x12345678;
    printf("%#x %#x\n",st.b,st.c);
    #if 0
    int a=0x12345678;
    //printf("%#x %#x\n",(char)a,(short)a);

    char *p=(char *)&a;
    printf("%#x %#x\n",*p,*(p+3));
    #endif
    return 0;
}

8.1 主机字节序到网络字节序

u_long htonl (u_long hostlong);//host to net
u_short htons (u_short short);  //掌握这个

8.2 网络字节序到主机字节序

u_long ntohl (u_long hostlong);//net to host
u_short ntohs (u_short short);

8.3 IP地址转换

typedef uint32_t in_addr_t;
struct in_addr {
    in_addr_t s_addr;
};

in_addr_t inet_addr(const char *cp);  //从人看的ip地址转为机器使用的32位无符号整数
char *inet_ntoa(struct in_addr in);  //从机器到人,即net网络转addr地址
int inet_aton(const char *cp, struct int_addr *inp);//从人到机器
如:inet_aton(192.168.1.0”,& mysock.sin_addr.s_addr); //设置地址

思考:Dos(拒绝式服务)攻击? (查阅)
拒绝服务攻击即是攻击者想办法让目标机器停止提供服务,是黑客常用的攻击手段之一。其实对网络带宽进行的消耗性攻击只是拒绝服务攻击的一小部分,只要能够对目标造成麻烦,使某些服务被暂停甚至主机死机,都属于拒绝服务攻击。拒绝服务攻击问题也一直得不到合理的解决,究其原因是因为网络协议本身的安全缺陷,从而拒绝服务攻击也成为了攻击者的终极手法。攻击者进行拒绝服务攻击,实际上让服务器实现两种效果:一是迫使服务器的缓冲区满,不接收新的请求;二是使用IP欺骗,迫使服务器把非法用户的连接复位,影响合法用户的连接。

九、TCP编程

9.1 三次握手流程图

在这里插入图片描述

服务器:
  socket:创建一个用与链接的套接字
  bind:绑定自己的ip地址和端口
  listen:监听,将主动套接字转为被动套接字
  accept:阻塞等待客户端链接,链接成功返回一个用于通信套接字
  recv:接收消息
  send:发送消息
  close:关闭文件描述符
客户端:
   socket:创建一个套接字
   填充结构体:填充服务器的ip和端口
   connect:阻塞等待链接服务器
   recv/send:接收/发送消息
   close:关闭

9.2 函数接口

1、 socket创建套接字

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
domain:协议族
AF_UNIX, AF_LOCAL  本地通信协议     unix(7)
    AF_INET             IPv4          ip(7)
    AF_INET6            IPv6        ipv6(7)
    type:套接字类型
SOCK_STREAM  流式套接字
SOCK_DGRAM  数据报套接字
SOCK_RAW   原始套接字 
protocol:协议  填0,自动匹配底层TCP或UDP等协议
系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:成功文件描述符 失败-1 更新errno

在这里插入图片描述

2、 bind绑定套接字

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);
功能:绑定套接字 - ip和端口
功能:
sockfd:套接字文件描述符
addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,
填充对应结构体-通信结构体由socket第一个参数确定)   

addrlen:结构体大小
返回值: 成功:0
失败:-1 更新errno

通用结构体:  
struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

ipv4的通信结构体:
struct sockaddr_in {
    sa_family_t    sin_family; /*AF_INET */
    in_port_t      sin_port;   /* 端口 */
    struct in_addr sin_addr;   /* ip地址 */
};
struct in_addr {
    uint32_t       s_addr;   
};

本地通信结构体:
struct sockaddr_un {
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_path[108];            /* 套接字文件 */
};

IPV4结构体使用方式一:
在这里插入图片描述IPV4结构体使用方式二:
在这里插入图片描述
IPV4结构体使用方式三:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc,char **argv)
{
	int sockfd;
	struct sockaddr_in mysock;
	sockfd = socket(AF_INET,SOCK_STREAM,0);  //获得fd
	bzero(&mysock,sizeof(mysock));  //初始化结构体
	mysock.sin_family = AF_INET;  //设置地址家族
	mysock.sin_port = htons(8080);  //设置端口
    inet_aton(192.168.1.0”,& mysock.sin_addr.s_addr); //设置地址
	// mysock.sin_addr.s_addr = inet_addr("192.168.1.0");  
	bind(sockfd,(struct sockaddr *)&mysock,sizeof(struct sockaddr);	/* bind的时候进行转化 */
	... ...
	return 0;
}

3、 listen监听

int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字,创建等待队列,用来存放未处理的客户连接请求。
参数:
sockfd:套接字
backlog:同时响应客户端请求链接的最大个数,不能写0.
    不同平台可同时链接的数不同,一般写6-8(队列1:保存正在连接)
    (队列2,连接上的客户端)
    返回值:成功 0   失败-1,更新errno

在这里插入图片描述

4、 accept等待连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
则accept()函数返回,返回一个用于通信的套接字文件;
参数:
Sockfd :套接字
addr: 链接客户端的ip和端口号
如果不需要关心具体是哪一个客户端,那么可以填NULL;
addrlen:结构体的大小
如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值: 
成功:文件描述符; //用于通信
失败:-1,更新errno

.会创建一个新的套接字文件描述符,后面的recv就是对新的套接字文件描述符操作。

5、 recv接收消息

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据 
参数: 
sockfd: acceptfd ;
buf  存放位置
len  大小
flags  一般填0,相当于read()函数
MSG_DONTWAIT  非阻塞
返回值: 
< 0  失败出错  更新errno
==0  表示客户端退出
>0   成功接收的字节个数

6、 send发送消息

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
sockfd:socket函数的返回值
buf:发送内容存放的地址
len:发送内存的长度
flags:如果填0,相当于write();
返回值: 
< 0  失败出错  更新errno
>0   成功发送的字节个数

7、 connect发送连接

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于连接服务器;
参数:
sockfd:socket函数的返回值
addr:填充的结构体是服务器端的;
addrlen:结构体的大小
返回值 
-1 失败,更新errno
正确 0

需要指明客户端是哪一个,accept如果不关心是哪个客户端,可以NULL忽略。
注:发送数据的大小最好与接收数据的大小一致。

server.c:
在这里插入图片描述

server.c:

在这里插入图片描述

8、 优化代码

1.去掉fget获取多余的‘\n’
      if(buf[strlen(buf)-1]=='\n')
         buf[strlen(buf)-1]='\0';
2.端口和ip地址通过命令行传参到代码中。
3.设置客户端退出,服务器结束循环接收。
    通过recv返回值为0判断客户端是否退出
4.设置来电显示功能,获取到请求链接服务器的客户端的ip和端口。
5.设置服务器端自动获取自己的ip地址。
   INADDR_ANY  "0.0.0.0"
6.实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。

在这里插入图片描述

十、实现:tcp实现ftp功能

模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。

项目功能介绍:
   均有服务器和客户端代码,基于TCP写的。
   在同一路径下,将客户端可执行代码复制到其他的路径下,接下来再不同的路径下运行服务器和客户端。
      相当于另外一台电脑在访问服务器。
客户端和服务器链接成功后出现以下提示:四个功能
***************list************** //列出服务器所在目录下的文件名(除目录不显示)
***********put filename********** //上传一个文件
***********get filename********** //重服务器所在路径下载文件
**************quit*************** //退出(可只退出客户端,服务器等待下一个客户端链接)

10.1 IO相关功能回顾

笔者整理的语雀相关IO知识库回顾

10.2 代码实现

server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int server_list(int acceptfd);
int server_get(int acceptfd, char *file_p);
int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("please input %s <port>\n", argv[0]);
        return -1;
    }
    //1.创建套接字 socket
    int sockfd, acceptfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);

    //填充ipv4的通信结构体
    struct sockaddr_in serveraddr, caddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[1]));
    //serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
    // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_addr.s_addr = INADDR_ANY;

    socklen_t len = sizeof(caddr);

    //&serveraddr -->struct sockaddr_in *

    //2.绑定套接字  bind  (绑定自己的ip和端口,便于别人找到自己)
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }
    printf("bind ok.\n");

    //3.listen  监听 将主动套接子变为被动等待
    if (listen(sockfd, 5) < 0)
    {
        perror("listen err.");
        return -1;
    }
    printf("listen ok.\n");
    while (1)
    {
        //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
        acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);

        if (acceptfd < 0)
        {
            perror("accept err.");
            return -1;
        }
        printf("accept ok.\n");

        //输出查看链接的客户端的ip和端口
        printf("ip:%s ,port:%d\n",
               inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

        //5.循环收发消息
        char buf[128];
        int recvbyte;
        while (1)
        {
            //接收消息
            recvbyte = recv(acceptfd, buf, sizeof(buf), 0);
            if (recvbyte < 0)
            {
                perror("recv err.");
                return -1;
            }
            else if (recvbyte == 0)
            {
                printf("client exit.\n");
                break;
            }
            else
            {
                printf("buf:%s\n", buf);
                switch (buf[0])
                {
                    case 'l':
                        server_list(acceptfd);
                        break;
                    case 'g':
                        server_get(acceptfd, buf + 4);
                        break;
                }
            }
        }
        close(acceptfd);
    }
    close(sockfd);
    return 0;
}

  //get filename
  //打开已存在的文件,读文件内容发送给客户端
  int server_get(int acceptfd, char *file_p)
{
    char buf[128];
    int ret;
    int fd = open(file_p, O_RDONLY);
    if (fd < 0)
    {
        perror("open error.");
        return -1;
    }
    while (1)
    {
        ret=read(fd, buf, sizeof(buf)-1);//预留一个位置补\0
        buf[ret]='\0';
        if(ret == 0)
            break;
        send(acceptfd, buf, sizeof(buf), 0);
    }
    strcpy(buf, "send ok");
    send(acceptfd, buf, sizeof(buf), 0);
    return 0;
}

  int server_list(int acceptfd)
{
    char buf[128];
    struct stat st;
    struct dirent *file = NULL;
    DIR *dir = opendir("./");
    if (dir == NULL)
    {
        perror("opendir err.");
        return -1;
    }
    while ((file = readdir(dir)) != NULL)
    {
        stat(file->d_name, &st);
        if ((st.st_mode & S_IFMT) == S_IFREG)
        {
            strcpy(buf, file->d_name);
            send(acceptfd, buf, sizeof(buf), 0);
        }
    }
    strcpy(buf, "send ok");
    send(acceptfd, buf, sizeof(buf), 0);
    return 0;
}

client.c

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

int client_list(int sockfd);
void list(void);
int client_get(int sockfd, char *file_p);
int main(int argc, char const *argv[])
{
    if (argc != 3)
    {
        printf("please input %s <ip> <port>\n", argv[0]);
        return -1;
    }
    //1.创建套接字 socket
    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);

    //填充ipv4的通信结构体  服务器端ip和端口
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);

    //2.请求链接  connect
    if (connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("connect err.");
        return -1;
    }
    printf("connect ok.\n");

    //3.循环收发消息
    char buf[128];
    int recvbyte;
    while (1)
    {
        list();
        fgets(buf, sizeof(buf), stdin);
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';

        if (strncmp(buf, "quit", 4) == 0)
            break;
        //发送请求
        send(sockfd, buf, sizeof(buf), 0);
        switch (buf[0])
        {
        case 'l':
            client_list(sockfd);
            break;
        case 'g':
            client_get(sockfd, buf + 4);
            break;
        }
    }
    close(sockfd);
    return 0;
}

//get filename
//打开新建一个文件,接收写文件
int client_get(int sockfd, char *file_p)
{
    char buf[128];
    //1.打开新建文件
    int fd = open(file_p, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open err.");
        return -1;
    }
    //2.循环接收写文件
    while (1)
    {
        recv(sockfd, buf, sizeof(buf), 0);
        if (strncmp(buf, "send ok", 7) == 0)
            break;
        //写文件
        write(fd,buf,strlen(buf));
    }
    return 0;
}

//list - 功能:显示服务器对应路径下的普通文件名
//  接收服务器发送过来的普通文件的名字
int client_list(int sockfd)
{
    char buf[128] = "";
    while (1)
    {
        recv(sockfd, buf, sizeof(buf), 0);
        if (strncmp(buf, "send ok", 7) == 0)
            break;
        printf("%s\n", buf);
    }
    return 0;
}

void list(void)
{
    printf("+++++++++++++++++++++++++++++++++++++++\n");
    printf("+++++++++++++  list   +++++++++++++++++\n");
    printf("+++++++++++++  get filename  ++++++++++\n");
    printf("+++++++++++++  put filename  ++++++++++\n");
    printf("+++++++++++++  quit    ++++++++++++++++\n");
    printf("+++++++++++++++++++++++++++++++++++++++\n");
}

十一、UDP编程

11.1 UDP流程图

在这里插入图片描述

udp流程:(类似发短信)
server:
创建数据报套接字(socket(,SOCK_DGRAM,)----->有手机
绑定网络信息(bind()---------------------->绑定号码(发短信知道发给谁)
接收信息(recvfrom()--------------------->接收短信
关闭套接字(close()----------------------->接收完毕

client:
创建数据报套接字(socket())----------------------->有手机
指定服务器的网络信息------------------------------>有对方号码
发送信息(sendto()---------------------------->发送短信
关闭套接字(close()--------------------------->发送完

11.2 函数接口

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
        0:客户端退出

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据

参数:
sockfd:套接字描述符
buf:发送缓存区的首地址                                                                  
len:发送缓存区的大小
flags:0
src_addr:接收端的网络信息结构体的指针
addrlen:接收端的网络信息结构体的大小


返回值: 
成功发送的字节个数
失败:-1

server.c
在这里插入图片描述
client.c
在这里插入图片描述
练习:实现如客户端发送"hello"给服务器端,服务器接着给客户端回,“recv:hello!!!”。

十二、UDP网络聊天室

注意:
1、对于TCP是先运行服务器,客户端才能运行。
2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系,
3、一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。
4、UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。
5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。

12.1 项目要求

利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
12.2 问题思考

12.2 问题思考

● 客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
● 有几种消息类型?
**登录:**服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
**聊天:**服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
**退出:**服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
● 服务器如何存储客户端的地址?
数据结构可以选择线性数据结构

链表节点结构体:
struct node{
struct sockaddr_in addr;
struct node *next;
};

消息对应的结构体(同一个协议)
    typedef struct msg_t
{
int type;//L  M  Q  
char name[32];//用户名
char text[128];//消息正文
}MSG_t;

● 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。

12.3 程序流程图

服务器端
在这里插入图片描述
客户端
在这里插入图片描述

十三、linux下I/O模型及特点

13.1 阻塞式IO

特点:最简单、最常用;效率低

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。
学习的读写函数在调用过程中会发生阻塞相关函数如下:
•读操作中的read、recv、recvfrom
读阻塞--》需要读缓冲区中有数据可读,读阻塞解除
•写操作中的write、send
写阻塞--》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。 
注意:sendto没有写阻塞
1)无sendto函数的原因:
sendto不是阻塞函数,本身udp通信不是面向链接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。
2)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。
•其他操作:accept、connect

13.2 非阻塞式IO

特点:可以处理多路IO;需要轮询,浪费CPU资源

•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。

在这里插入图片描述

13.2.1 通过函数参数设置非阻塞:

Recv函数最后一个参数写为0,为阻塞,写为MSG_DONTWAIT:表示非阻塞。
在这里插入图片描述

非阻塞,循环检测,是否有数据发过来,轮询消耗CPU资源。

13.2.2 通过fcntl函数设置文件描述符属性设置非阻塞:

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置文件描述符的属性
参数:
fd:文件描述符
cmd:功能选择 - 更改状态属性
F_SETFLF  设置文件描述符属性    第三个参设设置
F_GETFL   获取文件描述符属性    第三个参数忽略 返回的是获取到的属性
arg:设置的属性值
返回值:失败 -1 更新errno
成功根据功能选择返回

fcntl.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
    char buf[128];
    //设置文件描述符0的非阻塞属性
    int flags;
    flags = fcntl(0, F_GETFL); //获取原属性
    flags |= O_NONBLOCK;       //修改属性
    fcntl(0, F_SETFL, flags);  //修改的属性设置回去
    while (1)
    {
        fgets(buf, sizeof(buf),  stdin);
        printf("buf:%s\n", buf);
    }
    return 0;
}         

13.3 信号驱动IO (异步IO模型 非重点)

特点:异步通知模式,需要底层驱动的支持
● 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
● 应用程序收到信号后做异步处理即可。
● 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
标准模板

//设置将APP进程号提交给内核驱动
fcntl(fd,F_SETOWN,getpid());

//设置异步通知
int flags;
flags = fcntl(fd, F_GETFL); //获取原属性
flags |= O_ASYNC;       //设置异步通知
fcntl(fd, F_SETFL, flags);  //修改的属性设置回去

signal(SIGIO,handler);

举例:操作鼠标设备,当有输入的时候获取输入数据,没有输入时循环输出hello world。
鼠标设备路径 - /dev/input/mouse0
测试使用的是哪个鼠标设备:sudo cat /dev/input/mouse0
注意:执行代码时需要加sudo,普通用户没有操作设备的权限。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

int fd;
//信号处理函数
void handler(int sig)
{
    char buf[128];
    int ret = read(fd, buf, sizeof(buf) - 1);
    buf[ret] = '\0';
    printf("mouse:%s\n", buf);
}

int main(int argc, char const *argv[])
{
    //1.打开鼠标设备
    fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open mouse err.");
        return -1;
    }
    //2.将APP进程号告诉内核驱动
    fcntl(fd, F_SETOWN, getpid());

    //3.设置异步通信
    int flags;
    flags = fcntl(fd, F_GETFL); //获取原属性
    flags |= O_ASYNC;          //设置异步通知
    fcntl(fd, F_SETFL, flags);  //修改的属性设置回去

    //4.signal捕捉SIGIO信号
    signal(SIGIO, handler);
    while (1)
    {
        printf("hello world.\n");
        sleep(1);
    }
    return 0;
}

13.4 前三种使用场景假设总结

在这里插入图片描述
假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?

  1. 进到房间陪着孩子一起睡觉,孩子醒了会吵醒妈妈:不累,但是不能干别的了
  2. 时不时进房间看一下:简单,空闲时间还能干点别的,但是很累
  3. 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:互不耽误

13.5 IO多路复用

13.5.1 IO多路复用场景假设

假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?

  1. 不停进每个房间看一下:简单,空闲时间还能干点别的,但是很累
  2. 把三个房间的门都打开,在客厅睡觉,同时监听所有房间的哭声,如果被哭声吵醒,那么能准确定位某个房间,及时处理即可:既能得到休息,也能及时获知每个孩子的状态。

13.5.2 IO多路复用机制

● 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
● 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
● 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
● 比较好的方法是使用I/O多路复用技术。其基本思想是:
○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。
○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
○ 函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。

基本流程:
1. 先构造一张有关文件描述符的表(集合、数组); 
2. 将你关心的文件描述符加入到这个表中;
3. 然后调用一个函数。 select / poll 
4. 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候
该函数才返回(阻塞)5. 判断是哪一个或哪些文件描述符产生了事件(IO操作);
6. 做对应的逻辑处理;

13.5.3 实现IO多路复用的方式

13.5.3.1 select 实现IO多路复用
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
功能:select用于监测是哪个或哪些文件描述符产生事件;
有一个或多个同时产生时间返回值。
参数:nfds:    监测的最大文件描述个数
(这里是个数,使用的时候注意,与文件中最后一次打开的文件
描述符所对应的值的关系是什么?)
readfds:  读事件集合; //读(用的多)
writefds: 写事件集合;  //NULL表示不关心
exceptfds:异常事件集合;  
timeout:超时检测 1
         如果不做超时检测:传 NULL 
         select返回值:  <0 出错
>0 表示有事件产生;
如果设置了超时检测时间:&tv
select返回值:
<0 出错
>0 表示有事件产生;
==0 表示超时时间已到;

struct timeval {
         long    tv_sec;         /* seconds */
         long    tv_usec;        /* microseconds */
     };

void FD_CLR(int fd, fd_set *set);//将fd从表中清除
int  FD_ISSET(int fd, fd_set *set);//判断fd是否在表中
void FD_SET(int fd, fd_set *set);//将fd添加到表中
void FD_ZERO(fd_set *set);//清空表

select实现IO多路复用特点

1. 一个进程最多只能监听1024个文件描述符 (千级别)
2. select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源);
3. select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3G~4G是内核态,拷贝是非常耗时的);

练习:检测鼠标输入和键盘输入事件。
moues_key_select.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    //打开鼠标设备
    int fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open mouse err.");
        return -1;
    }
    //select 实现同时响应终端输入和鼠标输入
    //1.创建表
    fd_set readfds,tempfds;
    //清空表
    FD_ZERO(&readfds);
    //2.将关心的文件描述符添加到表中
    FD_SET(fd,&readfds);
    FD_SET(0,&readfds);

    int maxfd=fd;

    char buf[128];
    int ret;
    while(1)
    {
        //3.调用select函数检测事件
        tempfds=readfds;
        ret=select(maxfd+1,&tempfds,NULL,NULL,NULL);
        if(ret < 0)
        {
            perror("select err.");
            return -1;
        }
        //终端输入
        if(FD_ISSET(0,&tempfds))
        {
         fgets(buf,sizeof(buf),stdin);
         printf("key:%s\n",buf);
        }
        //鼠标输入
        if(FD_ISSET(fd,&tempfds))
        {
         int ret=read(fd,buf,sizeof(buf));
         printf("mouse:%s\n",buf);
        }
    }
    close(fd);
    return 0;
}

练习2:检测键盘和sockfd (TCP实现同时链接多个客户端)

修改 serever.c代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("please input %s <port>\n", argv[0]);
        return -1;
    }
    //1.创建套接字 socket
    int sockfd, acceptfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);

    //填充ipv4的通信结构体
    struct sockaddr_in serveraddr, caddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[1]));
    //serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
    // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_addr.s_addr = INADDR_ANY;

    socklen_t len = sizeof(caddr);
    //&serveraddr -->struct sockaddr_in *
    //2.绑定套接字  bind  (绑定自己的ip和端口,便于别人找到自己)
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }
    printf("bind ok.\n");

    //3.listen  监听 将主动套接子变为被动等待
    if (listen(sockfd, 5) < 0)
    {
        perror("listen err.");
        return -1;
    }
    printf("listen ok.\n");

    //select - 多个客户端链接一个服务器  用于链接的文件描述符是sockfd
    //请求,sockfd读到请求 检测sockfd的读事件
    //1.创建表
    fd_set readfds,tempfds;
    //清空表
    FD_ZERO(&readfds);
    //2.将关心文件描述符添加到表 0 sockfd=3
    FD_SET(0,&readfds);
    FD_SET(sockfd,&readfds);

    int maxfd=sockfd;

    //3.循环调用select检测
    int ret; 
    char buf[128];
    while (1)
    {
        tempfds=readfds;
        ret=select(maxfd+1,&tempfds,NULL,NULL,NULL);
        if(ret < 0)
        {
            perror("select err.");
            return -1;
        }
        //4.判断是否产生事件
        if(FD_ISSET(0,&tempfds))
        {
            fgets(buf,sizeof(buf),stdin);
            printf("key:%s\n",buf);
        }
        if(FD_ISSET(sockfd,&tempfds))
        {
            //sockfd产生事件,有客户端链接,需要调用accept进行链接
            //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
            acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
            if (acceptfd < 0)
             {
                 perror("accept err.");
                 return -1;
             }
            printf("accept ok.\n");

            //输出查看链接的客户端的ip和端口
            printf("%d ip:%s ,port:%d\n",acceptfd,
                   inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
        }
    }
    close(sockfd);
    return 0;
}

练习3:在实现练习2的基础上,再检测所有用于通信的acceptfd实现并发通信。
server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("please input %s <port>\n", argv[0]);
        return -1;
    }
    //1.创建套接字 socket
    int sockfd, acceptfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);

    //填充ipv4的通信结构体
    struct sockaddr_in serveraddr, caddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[1]));
    //serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
    // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_addr.s_addr = INADDR_ANY;

    socklen_t len = sizeof(caddr);

    //&serveraddr -->struct sockaddr_in *

    //2.绑定套接字  bind  (绑定自己的ip和端口,便于别人找到自己)
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }
    printf("bind ok.\n");

    //3.listen  监听 将主动套接子变为被动等待
    if (listen(sockfd, 5) < 0)
    {
        perror("listen err.");
        return -1;
    }
    printf("listen ok.\n");

    //功能:select 服务器响应多个客户端请求且进行通信
    //1.创建表
    fd_set readfds, tempfds;
    FD_ZERO(&readfds); //清空表
    //2.添加关心的文件描述符
    FD_SET(0, &readfds);
    FD_SET(sockfd, &readfds);

    int maxfd = sockfd, ret;
    char buf[128];
    int recvbyte;
    //3.循环调用select检测 ,select阻塞等待有事件产生返回
    while (1)
    {
        tempfds = readfds;
        ret = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
        if (ret < 0)
        {
            perror("select err.");
            return -1;
        }
        for (int i = 0; i <= maxfd; i++)
        {
            if (FD_ISSET(i, &tempfds))//循环遍历i即文件描述符的值,判断是否在监测表中
            {
                if (i == 0)//监测标准输入
                {
                    fgets(buf, sizeof(buf), stdin);
                    printf("key:%s\n", buf);
                    //将服务器段输入的数据作为通知的消息发送给所有链接的客户端
                    for(int j=4;j<=maxfd;j++)
                    {
                        if(FD_ISSET(j,&readfds))
                        {
                            send(j,buf,sizeof(buf),0);
                        }
                    }
                }
                else if (i == sockfd)//监测sockfd
                {
                    //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
                    acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);

                    if (acceptfd < 0)
                    {
                        perror("accept err.");
                        return -1;
                    }
                    printf("accept ok.\n");

                    //输出查看链接的客户端的ip和端口
                    printf("ip:%s ,port:%d\n",
                           inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

                    //需要将产生通信文件描述符添加到表中进行检测
                    FD_SET(acceptfd, &readfds);
                    if (maxfd < acceptfd)
                        maxfd = acceptfd;
                }
                else//监测所有acceptfd
                {
                    //接收消息
                    recvbyte = recv(i, buf, sizeof(buf), 0);
                    if (recvbyte < 0)
                    {
                        perror("recv err.");
                        // return -1;
                    }
                    else if (recvbyte == 0)
                    {
                        printf("%d client exit.\n",i);
                        FD_CLR(i, &readfds);//从文件描述符表中清除对应fd
                        close(i);
                        break;
                    }
                    else
                    {
                        printf("%d buf:%s\n",i, buf);
                    }
                }
            }
        }
    }
    close(sockfd);
    return 0;
}

client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    if (argc != 3)
    {
        printf("please input %s <ip> <port>\n", argv[0]);
        return -1;
    }
    //1.创建套接字 socket
    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);

    //填充ipv4的通信结构体  服务器端ip和端口
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);

    //&serveraddr -->struct sockaddr_in *

    //2.请求链接  connect
    if (connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("connect err.");
        return -1;
    }
    printf("connect ok.\n");

    //3.循环收发消息
    char buf[128];
    int recvbyte;
    pid_t pid = fork();//通过子进程实现全双工通信
    if (pid < 0)
    {
        perror("fork err.");
        return -1;
    }
    else if (pid == 0)
    {
        while (1)
        {

            fgets(buf, sizeof(buf), stdin);
            if (buf[strlen(buf) - 1] == '\n')
                buf[strlen(buf) - 1] = '\0';
            //发送消息
            send(sockfd, buf, sizeof(buf), 0);
        }
    }
    else
    {
        while (1)
        {
            //接收消息
            recvbyte = recv(sockfd, buf, sizeof(buf), 0);
            if (recvbyte < 0)
            {
                perror("recv err.");
                // return -1;
            }

            printf("buf:%s\n", buf);
        }
    }
    close(sockfd);
    return 0;
}

13.5.3.2 poll 实现IO多路复用
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参p数:
struct pollfd *fds
关心的文件描述符数组struct pollfd fds[N];
nfds:个数
timeout: 超时检测
毫秒级的:如果填10001秒
如果-1,阻塞

struct pollfd {
int   fd;         /* 检测的文件描述符 */
short events;     /* 检测事件 */
short revents;    /* 调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员。只需要判断这个成员的值就可以确定是否产生事件 */
};
使用如:
    struct pollfd fds[20]={};//poll()函数第一个参数传递的是fds的地址

事件: POLLIN :读事件
POLLOUT : 写事件
POLLERR:异常事件

poll实现IO多路复用的特点

1. 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定)
2. poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
3. poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可

server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
 #include <poll.h>

int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("please input %s <port>\n", argv[0]);
        return -1;
    }
    //1.创建套接字 socket
    int sockfd, acceptfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);

    //填充ipv4的通信结构体
    struct sockaddr_in serveraddr, caddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[1]));
    //serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
    // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_addr.s_addr = INADDR_ANY;

    socklen_t len = sizeof(caddr);

    //&serveraddr -->struct sockaddr_in *

    //2.绑定套接字  bind  (绑定自己的ip和端口,便于别人找到自己)
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }
    printf("bind ok.\n");

    //3.listen  监听 将主动套接子变为被动等待
    if (listen(sockfd, 5) < 0)
    {
        perror("listen err.");
        return -1;
    }
    printf("listen ok.\n");

    //功能:poll 服务器响应多个客户端请求且进行通信
    //1.创建表 -创建一个结构体数组
     struct pollfd fds[20]={};  //大小自己确定,没有限定个数
    //2.添加关心的文件描述符
     fds[0].fd = 0;
     fds[0].events=POLLIN;//读事件

     fds[1].fd=sockfd;
     fds[1].events=POLLIN;


    int n = 2, ret;
    char buf[128];
    int recvbyte;
    //3.循环调用poll检测 ,poll阻塞等待有事件产生返回 -1
    while (1)
    {
        ret = poll(fds, n, -1);
        if (ret < 0)
        {
            perror("poll err.");
            return -1;
        }
        //处理事件
        for (int i = 0; i < n; i++)
        {
            if (fds[i].revents == POLLIN)
            {
                if (fds[i].fd == 0)
                {
                    fgets(buf, sizeof(buf), stdin);
                    printf("key:%s\n", buf);
                    //将服务器段输入的数据作为通知的消息发送给所有链接的客户端
                    for(int j=2;j<n;j++)
                    {
                        send(fds[j].fd,buf,sizeof(buf),0);
                    }
                }
                else if (fds[i].fd == sockfd)
                {
                    //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
                    acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);

                    if (acceptfd < 0)
                    {
                        perror("accept err.");
                        return -1;
                    }
                    printf("accept ok.\n");

                    //输出查看链接的客户端的ip和端口
                    printf("ip:%s ,port:%d\n",
                           inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

                    //需要将产生通信文件描述符添加到表中进行检测
                    fds[n].fd=acceptfd;
                    fds[n].events=POLLIN;
                    n++;
                }
                else
                {
                    //接收消息
                    recvbyte = recv(fds[i].fd, buf, sizeof(buf), 0);
                    if (recvbyte < 0)
                    {
                        perror("recv err.");
                        // return -1;
                    }
                    else if (recvbyte == 0)
                    {
                        printf("%d client exit.\n",fds[i].fd);
                        close(fds[i].fd);
                        fds[i]=fds[n-1];
                        n--;
                        i--;              
                        break;
                    }
                    else
                    {
                        printf("%d buf:%s\n",fds[i].fd, buf);
                    }
                }
            }
        }
    }
    close(sockfd);
    return 0;
}

13.5.3.3 epoll 实现IO多路复用(异步)

epoll实现机制:(了解)

epoll的提出--》它所支持的文件描述符上限是系统可以最大打开的文件的数目;eg:1GB机器上,这个上限10万个左右。
每个fd上面有callback(回调函数)函数,只有活跃的fd才有主动调用callback,不需要轮询。

注意:
   Epoll处理高并发,百万级,不关心底层怎样实现,只需要会调用就可以。

在这里插入图片描述
函数接口

#include <sys/epoll.h>
int epoll_create(int size); 
功能:创建红黑树根节点
参数:size:不作为实际意义值 >0 即可
返回值:成功时返回epoll文件描述符,失败时返回-1int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll属性
epfd:epoll_create函数的返回句柄。
op:表示动作类型。有三个宏 来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已注册fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
Fd:需要监听的fd。
event:告诉内核需要监听什么事件
EPOLLIN:表示对应文件描述符可读
EPOLLOUT:可写
EPOLLPRI:有紧急数据可读;
EPOLLERR:错误;
EPOLLHUP:被挂断;
EPOLLET:触发方式,边缘触发;(默认使用边缘触发)
    ET模式:表示状态的变化;
返回值:成功时返回0,失败时返回-1

typedef union epoll_data{
    void* ptr;(无效)
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t events; / * Epoll事件* /
    epoll_data_t data; / *用户数据变量* /
};
//等待事件到来
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:等待事件的产生,类似于select的用法
epfd:句柄;
events:用来保存从内核得到事件的集合;
maxevents:表示每次能处理事件最大个数;
timeout:超时时间,毫秒,0立即返回,-1阻塞
成功时返回发生事件的文件描述个数,失败时返回-1
帮助理解:
1.epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
2.epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中 依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
 3.另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()
来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制, 迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

epoll实现IO多路复用的特点

•监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统)
•异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
•epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.

server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <poll.h>
 #include <sys/epoll.h>

int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("please input %s <port>\n", argv[0]);
        return -1;
    }
    //1.创建套接字 socket
    int sockfd, acceptfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);

    //填充ipv4的通信结构体
    struct sockaddr_in serveraddr, caddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[1]));
    //serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
    // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_addr.s_addr = INADDR_ANY;

    socklen_t len = sizeof(caddr);

    //&serveraddr -->struct sockaddr_in *

    //2.绑定套接字  bind  (绑定自己的ip和端口,便于别人找到自己)
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }
    printf("bind ok.\n");

    //3.listen  监听 将主动套接子变为被动等待
    if (listen(sockfd, 5) < 0)
    {
        perror("listen err.");
        return -1;
    }
    printf("listen ok.\n");

    //功能:epoll 服务器响应多个客户端请求且进行通信
    struct epoll_event event;       //暂时保存添加到树上的事件
    struct epoll_event revents[20]; //保存从链表中获取的产生的事件
    //1.创建表 -创建一颗树
    int epfd = epoll_create(1);

    //2.添加关心的文件描述符到树上  epoll_ctl
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = 0;
    epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);

    event.events = EPOLLIN | EPOLLET;
    event.data.fd = sockfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);

    int n = 2, ret;
    char buf[128];
    int recvbyte;
    //3.循环调用epoll_wait检测链表中是否有事件
    while (1)
    {
        //返回值是实际从链表中拿出来事件的个数
        ret = epoll_wait(epfd, revents, 20, -1);
        if (ret < 0)
        {
            perror("poll err.");
            return -1;
        }
        //处理事件
        for (int i = 0; i < ret; i++)
        {
            if (revents[i].data.fd == 0)
            {
                fgets(buf, sizeof(buf), stdin);
                printf("key:%s\n", buf);
            }
            else if (revents[i].data.fd == sockfd)
            {
                //4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
                acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);

                if (acceptfd < 0)
                {
                    perror("accept err.");
                    return -1;
                }
                printf("accept ok.\n");

                //输出查看链接的客户端的ip和端口
                printf("ip:%s ,port:%d\n",
                       inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

                //需要将产生通信文件描述符添加到树
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = acceptfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &event);
            }
            else
            {
                //接收消息
                recvbyte = recv(revents[i].data.fd, buf, sizeof(buf), 0);
                if (recvbyte < 0)
                {
                    perror("recv err.");
                    // return -1;
                }
                else if (recvbyte == 0)
                {
                    printf("%d client exit.\n", revents[i].data.fd);
                    close(revents[i].data.fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, revents[i].data.fd,NULL);
                    break;
                }
                else
                {
                    printf("%d buf:%s\n",revents[i].data.fd, buf);
                }
            }
        }
    }
    close(sockfd);
    return 0;
}

十四、服务器模型

● 在网络程序里面,通常都是一个服务器处理多个客户机。
● 为了处理多个客户机的请求, 服务器端的程序有不同的处理方式。

14.1 循环服务器模型

同一个时刻只能响应一个客户端的请求,伪代码如下:

socket()
    bind();
listen();
while(1)
{
    accept();
    while(1)
    {
        process(); //处理
    }
    close();
}

14.2 并发服务器模型

同一个时刻可以响应多个客户端的请求,常用的模型有多进程模型/多线程模型/IO多路复用模型。

14.2.1 多进程模型

每来一个客户端连接,开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大,更推荐使用线程模型。伪代码如下:

socket()
    bind();
listen();
while(1)
{
    accept();
    if(fork() == 0)  //子进程
    {
        while(1)
        {
            process();
        }
        close(client_fd);
        exit();
    }
    else
    {
    }
}

注意:收到客户端消息后,打印下是来自哪个客户端的数据(来电显示)
使用SIGCHLD来处理子进程结束的信号,信号函数中回收进程资源。

14.2.2 多线程模型

来一个客户端连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源较少,属于使用比较广泛的模型:

socket()
    bind();
listen();
while(1)
{
    accept();
    pthread_create();
}

14.2.3 IO多路复用模型

借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起了稍显繁琐。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值