0%

Linux系统编程_网络

1 TCP/IP协议

TCP/IP协议套件是一个分层联网协议,它包括因特网协议(ip)和位于其上层的各个协议层。

1.1 OSI七层模型和TCP/IP模型

各层协议主要有:

  • 应用层协议: FTP(文件传输协议)、HTTP(超文本传输协议)、NFS(网络文件系统)
  • 传输层协议: TCP (传输控制协议)、UDP(用户数据报协议)
  • 网络层:IP(英特网互联协议)ICMP(英特网控制报文协议ping) 、IGMP(英特网组管理协议)
  • 链路层协议:ARP(地址解析协议 通过ip找mac地址)、RARP:(反向地址解析协议 通过mac找ip)
1.2 TCP报文格式

  • 序列号:该报文的序列号,标识TCP发端向TCP接收端发送的数据字节流
  • 确认序列号:如果设定了ACK,那么从这个字段包含了接收放期望从发送方接收到的下一个数据字节的序列号
  • 首部长度:该字段标识了TCP报文首部长度,该字段4个比特网位,则表示首部长度最大可达60字节
  • 保留位:该字段有4位未使用的比特位
  • 控制位:
    • CWR:拥塞窗口减小标记
    • ECE:显示的拥塞通知回显标记
    • URG:若设置了该位,则紧急指针字段包含的信息有效
    • ACK:若设置了该位,则确认序列号字段包含的信息有效
    • PSH:将所有收到的数据发送接收的进程
    • RST:重置连接
    • SYN:同步序列号
    • FIN:发送端提示已经完成了发送任务
  • 窗口大小:该字段用在接收端发送ACK确认时提示自己可接受数据的空间大小
  • 校验和:16位的检验
1.3 TCP和UDP的区别
  • 连接
    • TCP是面向连接的传输层协议,即传输数据之前必须先建立好连接。
    • UDP则是无连接。
  • 服务对象
    • TCP是点对点的两点间服务,即一条TCP连接只能有两个端点;
    • UDP支持一对一,一对多,多对一,多对多的交互通信。
  • 可靠性
    • TCP是可靠交付,无差错,不丢失,不重复,按序到达。
    • UDP是尽最大努力交付,不保证可靠交付。
  • 拥塞控制,流量控制机制
    • TCP有拥塞控制和流量控制保证数据传输的安全性。
    • UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。
  • 报文长度
    • TCP是动态报文长度,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。
    • UDP面向报文,不合并,不拆分,保留上面传下来报文的边界。
  • 首部开销
    • TCP首部开销大,首部20个字节。
    • UDP首部开销小,8字节。(源端口,目的端口,数据长度,校验和)
  • TCP和UDP适用场景
    • 从特点上我们已经知道,TCP 是可靠的但传输速度慢,UDP 是不可靠的但传输速度快。因此在选用具体协议通信时,应该根据通信数据的要求而决定。若通信数据完整性需让位与通信实时性,则应该选用TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)。
1.4 TCP三次握手/四次挥手
  • 只有SYN位置1表示连接请求。
  • 只有ACK置1表示ACK报文段,携带数据时会消耗序号seq,不携带则不消耗
  • ACK和SYN都置1,不能携带数据,但消耗1个序号seq
1.4.1 三次握手
  • 1、Client将标志位SYN置为1,随机产生一个序号值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
  • 2、Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYNACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
  • 3、Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

三次握手的原因:

  • 建立连接
  • 第三次要回ACK的原因:如果没有第三次回ACK,会导致已经失效的连接请求报文突然又传输到服务器端,又建立客户端与服务器的连接,但客户端又不发送数据,此时导致的服务器资源浪费。所谓已经失效的连接请求是指客户端发出的连接请求滞留在网络中,超时后但又被服务器接收到。
1.4.2 四次挥手

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再发送到数据了,但是在这个TCP连接上仍然能够收数据,直到另一方也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

过程:

  • 1.数据传输结束后,客户端的应用进程发出连接释放FIN报文段,并停止发送数据,客户端进入FIN_WAIT_1状态,此时客户端依然可以接收服务器发送来的数据。
  • 2.服务器接收到FIN后,发送一个ACK给客户端,确认序号为收到的序号+1,服务器进入CLOSE_WAIT状态。客户端收到后进入FIN_WAIT_2状态。
  • 3.当服务器没有数据要发送时,服务器发送一个FIN报文,此时服务器进入LAST_ACK状态,等待客户端的确认
  • 4.客户端收到服务器的FIN报文后,给服务器发送一个ACK报文,确认序列号为收到的序号+1。此时客户端进入TIME_WAIT状态,等待2MSL(MSL:报文段最大生存时间),然后关闭连接。

应用层可以使用系统调用函数read函数==0来判断对端是否关闭连接。为什么要等待2MSL才关闭链接:

  • 为保证客户端发送的最后一个ACK报文段能够到达服务器。超时重传
  • 经过2MSL时间,可以使本次连接持续时间内所以产生的报文段(主要针对滞留在网络中的)都失效,这样就可避免已失效的本次链接请求影响下一次链接请求。
1.4.3 TCP状态转换图

  • CLOSED:表示初始状态。
  • LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
  • SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
  • SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
  • ESTABLISHED:表示连接已经建立。
  • FIN_WAIT_1: FIN_WAIT_1FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
    • FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
    • FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
  • FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发,既socket上还有数据流动。

  • TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

  • CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

  • CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。(服务器)
  • LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。(服务器)

2. 网络socket先备知识点

2.1 端口

传输层协议的任务是向位于不同主机上的应用程序提供端到端的通信服务。为完成这个任务,传输层需要采用一中方法来区分主机上的应用程序,这种区分工作就由一个16位端口号来完成,即用来标识应用程序(进程)。众所周知,一些端口已经固定分配给一些应用,如22——ssh80-HTTP

  • port:2个字节 0-65535。
  • 0-1023为 知名端口(不可更改)。
  • 自定义端口 1024 - 65535
  • 查看端口使用情况:netstat
2.2 字节序

IP地址和端口号是整数值,这些值在网络传递中的一个问题是不同的硬件结构会以不同的顺序来存储一个多字节整数的字节:

  • 存储整数时在最小内存地址先存储最高位的称为大端,低位存低地址,高位存高地址(也叫网络字节序)
  • 存储整数时在最小内存地址先存储最低位的称为小端,低位存高地址,高位存低地址

1
2
3
4
5
#include <arpa/inet.h>
unit32_t htonl(unit32_t hostlong) //host to newwork long
unit16_t htons(unit16_t hostshort)
unit32_t ntohl(unit32_t netlong)
nuit16_t ntohs(unit16_t netshort)
示例deamon:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#incldue <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#incldue <apra/inet.h>

int main()
{
char buf[4]={192,168,1,2};
//int is 4B,char is 1B,can cover by int just right
int num =*(int*)buf;
int sum=htonl(num);
unsigned char *p=&num;
printf("%d %d %d %d",*p,*(p+1),*(p+2),*(p=3));
int sum1=ntohl(num);
p=&sum;
printf("%d %d %d %d",*p,*(p+1),*(p+2),*(p=3));
return 0;
}

2.3 ip转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能: 将点分十进制串 转成32位网络大端的数据("192.168.1.2" ==> )
参数:
af :
AF_INET IPV4
AF_INET6 IPV6
src: 点分十进制串的首地址
dst : 32位网络数据的地址
成功返回1

#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
功能: 将32位大端的网络数据转成点分十进制串
参数:
af : AF_INET
src : 32位大端的网络数 地址
dst : 存储点分十进制串 地址
size : 存储点分制串数组的大小 ,一般为16
返回值: 存储点分制串数组首地址

示例:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <arpa/inet.h>
int main(){
char buf[]="192.16.1.2";
unsigned int num=0;
inet_pton(AF_INET,buf,&num);
unsigned char* p=(unsigned char*)&num;
printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p=3));
cahr ip[16]="";
printf("%s\n",inet_ntop(AF_INET,&num,ip,16));
}

2.4 套接字结构体

网络通信只需解决3个问题(其它组包过程协议会帮我们完成):协议、ip、端口。而在三个统一在对应的结构体封装,我们只需要创建一个已初始化的结构体即可。

2.4.1 IPv4结构体
1
2
3
4
5
6
7
8
strcut in_addr{				//地址结构体,存储ip地址
in_addr_t s_addr; //无符号32位整型
};
struct sockaddr_in{
sa_family_t sin_family; //协议类型:AF_INET
in_port_t sin_port; //端口
struct in_addr sin_addr; //ip地址
};
2.4.2 IPv6结构体
1
2
3
4
5
6
7
8
9
10
struct in6_addr{
uint8_t s6_addr[16];
};
struct sockaddr_in6{
sa_family_t sin6_family; //AF_INET6
in_port_t sin6_port;
unit32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
2.4.3 Unix domain结构体
1
2
3
4
struct sockaddr_un{
sa_family_t sun_family; //AF_UNIX
char sun_path[108];
};
2.4.4 通用结构体
1
2
3
4
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};

3 socket套接字

socket是一钟IPC方法,它允许位于同一主机或使用网络连接起来的不同主机上的应用程序之间能够交换数据。常见的就是: - UNIX domain(AF_UNIX)允许在同一主机上的应用程序之间通信。 - IPv4(AF_INET)domain允许在使用因特网协议第四版(IPv4)网络连接起来的应用程序之间通信 - IPv6(AF_INET6)domain允许在使用因特网协议第六版(IPv6)网络连接起来的应用程序之间通信

这里我们主要对网络间通信做说明

同样在网络通信中socket支持两种形式的传输一个是流socket(TCP socket),另一个是数据报socket(UDP socket),顾名思义,它们在传输层走的协议分别是TCP和UDP

3.1 socket:TCP/UDP服务器通信步骤

TCP服务器通信步骤:

  • 服务器:创建套接字 socket-> 绑定 bind->监听 listen->提取 accept->读写->关闭
  • 客户端:创建套接字 socket-> 建立连接 connect

UDP服务器通信步骤:

  • 服务器: 创建报式套接字socket-> 绑定bind-> 读写-> 关闭
  • 客户端: 创建报式套接字socket-> 读写-> 关闭
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    发数据:
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
    const struct sockaddr *dest_addr, socklen_t addrlen);
    dest_addr: 目的地的地址信息
    addrlen: 结构体大小
    收数据:
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
    struct sockaddr *src_addr, socklen_t *addrlen);
    src_addr: 对方的地址信息
    addrlen: 结构体大小的地址
3.1.1 创建套接字API

无论是服务器还是客户端都要创建socket

1
2
3
4
5
6
7
8
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
创建套接字
参数:
domain:AF_INET
type: SOCK_STREAM 流式套接字 用于tcp通信
protocol: 0
成功返回文件描述符,失败返回-1
3.1.2 bind绑定

给套接字绑定固定的端口和ip

1
2
3
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
成功返回0 失败返回;-1
3.1.3 listen:使服务器处于监听状态

服务器在创建完socket和绑定了相应的端口和ip地址后,要置于监听状态,监听网络中客户端的连接请求:

1
2
3
int listen(int sockfd, int backlog);
参数:
backlog : 已完成连接队列和未完成连接队里数之和的最大值 128
3.1.4 accept:响应连接请求,并建立连接

如果连接队列没有新的连接,accept会阻塞

1
2
3
4
5
6
7
8
9
int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
功能: 从已完成连接队列提取新的连接
参数:
socket : 套接字
address : 获取的客户端的的ip和端口信息 iPv4套接字结构体地址
address_len: iPv4套接字结构体的大小的地址
返回值: 新的已连接套接字的文件描述符
socklen_t len = sizeof(struct sockaddr );
3.1.5 connect:客户端连接服务器
1
2
3
4
5
6
int connect(int sockfd , const struct sockaddr *addr,
socklen_t addrlen);
功能: 连接服务器
sockfd: socket套接字
addr: ipv4套接字结构体的地址
addrlen: ipv4套接字结构体的长
3.1.6 socket的发送接收函数

除了常用的readwrite函数以外,还有专用于套接字的I/O系统调用recv()send()(这些一般来说适用于TCP socket)

1
2
3
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);//flags==MSG_PEEK 读数据不会删除缓冲区的数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);//flags=1 紧急数据
flag参数:(详细请查书)

flags参数的取值和作用:

  • MSG_OOB:用于接收带外数据。带外数据指的是在TCP流中紧急的和高优先级的数据,通常用于紧急情况下的通信,比如错误报告和紧急状态的消息。由于带外数据具有更高的优先级,因此接收到带外数据时,应该立即进行处理,而不应该等待完成之后再处理。MSG_OOB标志用于区分普通数据和带外数据,如果没有设置该标志,则recv函数仅能接收普通数据。
  • MSG_PEEK:将数据从套接字的接收缓冲区中读出,但不将其删除。也就是说,该标志会将数据复制到指定的缓冲区,但不会影响接收缓冲区的状态,下一次调用recv函数时,仍然可以接收到相同的数据。该标志通常用于预览数据,检查数据包头信息等。
  • MSG_WAITALL:指定接收数据的长度,直到接收到指定长度的数据包之后才返回,否则会一直等待直到接收到足够长度的数据包。该标记通常用于接收固定长度的报文。
  • MSG_DONTWAIT:设置非阻塞模式,使recv函数在没有接收到数据时立即返回,而不是一直等待直到有数据到来。在非阻塞模式下,recv函数返回-1并设置errno为EAGAIN表示没有数据可读。
  • MSG_TRUNC:指示接收缓冲区空间不足时丢弃数据,并返回接收到的数据长度。该标志通常用于确保缓冲区可以容纳完整的数据包,在缓冲区不够存放完整的数据包时,在不保存多余数据的情况下截断数据。
  • MSG_ERRQUEUE:用于处理错误消息,如果有错误消息需要发送给应用程序,就通过控制消息(CMSG)机制返回到应用程序中。
  • MSG_NOSIGNAL:如果在发送数据时连接已经中断,不产生SIGPIPE信号,返回-1并设置errno为EPIPE。默认情况下,当发送到已关闭的套接字时会产生SIGPIPE信号,如果设置了该标志,则可以避免信号的产生,而是以返回错误的方式通知应用程序连接已经中断。
3.2 示例:简易客户端实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <sys/socket.h>
int main(int argc,char*argv[])
{
int sock_fd;
sock_fd=socket(AF_INET,SOCK_STREAM,0);
//建立连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(8000);
inet_pton(AF_INET,"169.254.128.147",&addr.sin_addr.s_addr);
int ero=connect(sock_fd,(struct sockaddr*)&addr,seziof(addr));
if(0!=ero)
{
printf("连接失败\n");
perror("connect");
return 0;
}
char buf[1024]="";
while(1)
{
int n =read(STDIN_FILENO,buf,seziof(buf));
write(sock_fd,buf,sizeof(buf));
n= read(sock_fd,buf,sizeof(buf));
write(STDOUT_FIFLENO,buf,n);
printf("\n");
}
close(sock_fd);
return 0;
}
3.3 示例:简易服务器实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main(int argc,char* argv[])
{
//创建套接字
int sfd=socket(AF_INET,SOCK_STREAM,0);
//绑定
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8000);
inet_pton(AF_INET,"192,168.3.8",&addr.sin_addr.s_addr);
bind(sfd,(struct sockaddr*)&addr,sizeof(addr));
//监听
listen(sfd,128);
//提取
struct sockaddr_in cliaddr;
socklen_t len=sizeof(cliaddr);
int cfd=accept(sfd,(struct sockaddr*)&cliaddr,&len);
char ip[16]="";
printf("建立连接,对端ip:%s\n",inet_ntop(AF_INET,&(cliaddr.sin_addr.s_addr),ip,16));
//读写
char buf[1024]="";
while(1)
{
memset(buf,0,sizeof(buf));
int n=read(STDIN_FILENO,buf,sezifo(buf));
write(cfd,buf,n);
n=read(cfd,buf,sizeof(buf));
printf("%s\n",buf);
}
//关闭
close(sfd);
close(cfd);
}

4 服务器示例

4.1 setsockopt函数
1
2
3
4
int setsockopt( int socket, int level, int option_name,const void *option_value, size_t ,ption_len);
第一个参数socket是套接字描述符。
第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。
option_name指定准备设置的选项,option_name可以有哪些取值,这取决于level

option_name:

  • SO_DEBUG,打开或关闭调试信息。当option_value不等于0时,打开,否则,关闭

  • SO_REUSEADDR,打开或关闭地址端口复用功能,当option_value不等于0时,打开,否则,关闭。

  • SO_DONTROUTE,打开或关闭路由查找功能。

  • SO_BROADCAST,允许或禁止发送广播数据。

  • SO_SNDBUF,设置发送缓冲区的大小。发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。

  • SO_RCVBUF,设置接收缓冲区的大小。接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)和256字节。

  • SO_KEEPALIVE,套接字保活。如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时 器,否则关闭保活定时器。

  • SO_OOBINLINE,紧急数据放入普通数据流。

  • SO_NO_CHECK,打开或关闭校验和。

4.1 心跳包

在TCP网络通信中,经常会出现客户端和服务器之间的非正常断开,需要实时检测查询链接状态。常用的解决方法就是在程序中加入心跳机制。如果对方异常断开,本机检测不到,一直等待,浪费资源。需要设置tcp的保持连接,作用就是每隔一定的时间间隔发送探测分节,如果连续发送多个探测分节对方还未回,就将次连接断开

1
2
3
//心跳包
int keepAlive = 1;
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));
注: - 心跳包: 最小粒度 - 乒乓包: 携带比较多的数据的心跳包

4.2 IO端口复用

实际上,默认的情况下,如果一个网络应用程序的一个套接字 绑定了一个端口( 占用了 8000 ),这时候,别的套接字就无法使用这个端口( 8000 )。或者关闭了这个应用程序,但处于TIME_WAIT需等待2MSL,为避免等待,就要使用IO端口复用。 设置端口复用的办法:在套接字绑定前,加以下两句:

1
2
int opt=1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
SO_REUSEADDR可以用在以下四种情况下。 (摘自《Unix网络编程》卷一,即UNPv1)

  • 1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
  • 2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可以测试这种情况。
  • 3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
  • 4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。
4.3 多进程版服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>
#include "wrap.h"

//信号处理函数
void free_process(int sig)
{
pid_t pid;
while(1)
{
pid=waitpid(-1,NULL,WNOHANG);
if(pid<=0)
break; //小于0,代表子进程全部退出,等于0,没有子进程退出
else
printf("child pid=%d\n",pid);
}
}

//使用已包裹函数实现socket
int main(int argc,char* argv[])
{ //crtl+c率先于信号注册杀死进程,SIGCHLD信号默认是忽略的
//导致无法捕捉该信号,因此先屏蔽
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK,&set,NULL);
//创建套接字,绑定
int lfd=tcp4bind(8000,NULL);
//监听
Listen(lfd,128);
//提取,回摄服务器
struct sockaddr_int cliaddr;
socklen_t len=sizeof(cliaddr);
while(1)
{
char ip[16]="";
int cfd=Accept(lfd,(struct sockaddr*)&cliaddr,&len);
printf("建立连接,对端ip:%s\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16))
//创建子进程
pid_t pid;
pid=fork();
if(pid<0)
{
perror("fork");
exit(0);
}
else if(0==pid)
{
//子进程
char buf[1024]="";
int n=read(STDIN_FILENO,buf,sezifo(buf));
write(cfd,buf,n);
n=read(cfd,buf,sizeof(buf));
if(n<0)
{
perror(”read");
close(cfd);
exit(0);
}
else if(0==n)
{
printf("客户端已关闭\n");
eixt(0)
}
}
else{
//父进程
close(fd);
//回收,注册信号,避免父进程粗赛无法提取连接进程
strcut sigcation art;
act.sa.hindler=0;
sigemptyset(&act,sa_fmask);
sigaction(SIGCHLD,&Aact,NULL);
//基础屏蔽
sigcrocmask(SIG_UNBLOCK,%SET,null);
}
}
return 0;
}
4.4 多线程版实现服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>
#include "wrap.h"

typedef struct client_info{
int c_cfd;
struct sockaddr_in c_addr;
}INFO;

void* Client_fun(void* arg);

//使用已包裹函数实现socket
int main(int argc,char* argv[])
{
//分离线程,让系统回收
pthread_attr_t attr;
pthread_attr_init(&attr);
int m= pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if(m!=0)
printf("setdetachstate failed\n");
//创建套接字,绑定
int lfd=tcp4bind(8000,NULL);
//监听
Listen(lfd,128);
//提取,回摄服务器
struct sockaddr_int cliaddr;
socklen_t len=sizeof(cliaddr);
INFO* c_info;
while(1)
{

int cfd=Accept(lfd,(struct sockaddr*)&cliaddr,&len);
//要传入线程的参数
c_info=(INFO*)malloc(sizeof(INFO));
c_info->c_cfd=cfd;
c_info->c_addr=cliaddr;

//创建线程
pthread_t pth;
int n=pthread_create(&pth,&attr,Client_fun,c_info);
if(n!=0)
printf("pthread_create failed\n");
}
pthread_attr_destroy(&attr);
return 0;
}

void *Client_fun(void* arg)
{
INFO* info=(INFO*)arg;
char ip[16]="";
printf("client ip:%s,port:%d\n",inet_ntop(AP_INET,
&(info->c_addr.sin_addr.s_addr),ip,16),ntohs(info->c_addr.sin_port));

int cfd=info->c_cfd;
char buf[1024]="";
memset(buf,0,sizeof(buf));
while(1){
int n=read(STDIN_FILENO,buf,sizeof(buf));
write(cfd,buf,n);
n=read(cfd,buf,sizeof(buf));
if(n<0)
{
perror("");
break;
}
else if(n==0)
{
printf("client close\n");
break;
}
else
{
printf("%s\n",buf);
}
}
close(cfd);
free(info);
}

5 高并发技术(IO多路复用)

5.1 多路IO转接(复用)服务器

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件描述符的属性变化,查看它们是否准备好执行I/O。

主要使用的方法有三种:select、poll、epoll

5.2 select
5.2.1 select原理

select拜托内核去监听cfdlfd,在应用层上我们需要人为的备份一份fd_set集合称为oldset,用于每次监听传入的集合。

select进入内核中后,将只会保留那些发生改变的文件描述符,并返回给应用层,此时,我们只需遍历一下返回的集合,就知道要做哪些操作(是读还是写,还是提取新cfd)。后再将更新后(既发生了lfd提取,如上图可增加7之后的文件描述符)或无需更新(未发生提取)的备份oldset传入select,再次监听。循环如此

5.2.2 select接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
select的API
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能: 监听多个文件描述符的属性变化(读,写,异常)
参数:
nfds : 最大文件描述符+1(select会遍历的文件描述符的取值范围,因为文件描述符是位图存储(默认共1024个,012默认是标准输入输出错误输出占有))
readfds : 需要监听的读的文件描述符存放集合
writefds :需要监听的写的文件描述符存放集合
exceptfds : 需要监听的异常的文件描述符存放集合
timeout: 多长时间监听一次 固定的时间,限时等待 NULL 永久监听
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */微妙
};
返回值: 返回的是变化的文件描述符的个数
注意: 变化的文件描述符会存在监听的集合中,未变化的文件描述符会从集合中删除

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);
5.2.3 select的优缺点

优点

  • select使用的较为广泛,在unix和windows上都标准化了,支持跨平台,移植性较好
  • select的超市精度可为微妙级,比poll和epoll的秒级好。

缺点:

  • 由于 FD_SETSIZE的限制,文监听的件描述符有1024数量的限制,若要修改,要重新编译程序
  • 只是返回变化的文件描述符的个数,具体哪个那个变化需要完整遍历集合
  • ** select和poll一样,每次都需要将需要监听的文件描述集合由应用层符拷贝到内核。当应对大量的文件描述符时,这种从用户空间到内核空间的来回拷贝数据回耗费大量时间。(select和poll共同点)**
  • ** select必须额外维护一个数据结构,这样在再次调用select时才能将其重新传入内核。**
  • select调用完成后,程序必须按顺序在集合查找发生变化的文件描述符,效率低
    • 假设现在 4-1023个文件描述符需要监听,5-1000这些文件描述发来消息,ok,这种情况没问题
    • 假设现在 4-1023个文件描述符需要监听,但是只有 5,1002 发来消息,无解,只能一一遍历
5.3 epoll

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

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

简而言之,epoll具有的特点:

  • 同poll一样没有文件描述符的限制
  • 下次监听不需要将需要监听的文件描述符从应用层再次拷贝到核
  • 会返回已经变化的的文件描述符,不用我们去遍历红黑树
5.3.1 epoll的API

①创建一颗红黑树的句柄

1
2
3
4
#include <sys/epoll.h>
int epoll_create(int size)
size:监听数目
返回值:成功:非负文件描述符,失败:-1,设置响应的errno

②将需要监听的文件描述符上树,下树、修改操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
epfd : 树的句柄
op : EPOLL_CTL_ADD 上树 EPOLL_CTL_DEL 下树 EPOLL_CTL_MOD 修改
fd : 上树,下树的文件描述符
event : 上树的节点
struct epoll_event {
uint32_t events; /* Epoll events */ 需要监听的事件
epoll_data_t data; /* User data variable */ 需要监听的文件描述符
};
相应联合体:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
例:
将cfd上树
int epfd = epoll_create(1);
struct epoll_event ev;
ev. data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD,cfd, &ev);
struct epoll_event结构中,events可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

③监听:timeout=-1时阻塞

1
2
3
4
5
6
7
8
9
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
功能: 监听树上文件描述符的变化
epfd : 数的句柄
events : 接收变化的节点(结构体)的数组的首地址,所以我们要创建一个数组去接收他
maxevents : 数组元素的个数
timeout : -1 永久监听,阻塞直到有文件描述符发生变化 大于等于0 限时等待
返回值: 返回的是变化的文件描述符个数

5.3.2 epoll_wait的两个工作方式

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。(epoll_wait是一个系统调用,尽量少调用。所以尽量使用边沿触发)

  • 水平触发(level-trggered):
    • 只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,
    • 当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知
    • LT模式支持阻塞和非阻塞两种方式,即设置timeout的值。epoll默认的模式是LT
  • 边缘触发(edge-triggered)
    • 当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知
    • 当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知

epoll默认为水平触发,若改为边沿,则只需ev.events=EPOLLIN | EPOLLET。水平触发,只要缓存区有数据epoll_wait就会被触发,边沿触发数据来一次只触发一次。

  • 水平触发和边缘触发模式区别
    • 读缓冲区刚开始是空的,然后读缓冲区写入2KB数据,水平触发和边缘触发模式此时都会发出可读信号,收到信号通知后,读取了1kb的数据,读缓冲区还剩余1KB数据,水平触发会再次进行通知,而边缘触发不会再进行通知。所以,边缘触发需要一次性的把缓冲区的数据读完为止,也就是一直读,直到读到EGAIN为止,EGAIN说明缓冲区已经空了,因为这一点,边缘触发需要设置文件句柄为非阻塞

注:(即指在使用边缘触发时,由于读数据只触发一次,这个时候要求一次性将数据读完,所以while循环读,读到最后。又因为read默认带阻塞,为不能让read阻塞(因为阻塞的话不能再去监听),因此设置cfd为非阻塞,这样read读到最后一次返回值为-1.判断errno的值为EAGAIN,代表数据读干净,然后再进行监听)

工作中: 一般来说,cfd采用边沿触发 + 非阻塞 = 高速模式;lfd采用水平触发 注意:边缘触发,目的是减少epoll_wait的调用次数,提升程序效率

5.3.3 边缘触发时文件描述符饥饿现象
  • 什么是文件描述符饥饿现象:现在我么使用边缘触发通知监视多个文件描述符,其中一个就绪态的描述符上有着大量的输入存在,此时我们的程序通过非阻塞的式的读操作将所有输入都读取,那么此时就会使其他文件描述符处于饥饿状态的风险,即我们再次检查文件描述符之前有很长一段处理时间)
  • 解决方法;
    • 应用程序维护一个列表,调用epoll_wait监视描述符时,将处于就绪状态的描述符添加到应用程序维护的列表中;然后对列表中的文件描述符进行一定限度的I/O操作(可以采用轮转调度方式循环处理),当相关非阻塞I/O系统调用出现EAGAIN时,这些文件描述符就能从列表删除,这样就避免了单个文件描述符长时间占有CPU资源导致其他文件描述符的饥饿现象。
5.3.4 epoll监听管道读描述符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(){
//创建无名管道,fd[0]为读,fd[1]为写
int fd[2];
int ret =pipe(fd);
if(ret!=0)
{
perror("pipe");
exit(0);
}
//创建epoll,fd[0]上树
int epollfd=epoll_create(128);
if(epollfd<0)
{
perror("epoll");
exit(0);
}
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=fd[0];
ret=epoll_ctl(epollfd,EPOLL_CTL_ADD,fd[0],&ev);
if(0!=ret)
{
perror("epoll_ctl");
exit(0);
}
//监听数组
struct epoll_event change fd[64];
//创建子进程
pid_t pid;
pid=fork();
if(pid<0)
{
perror("fork");
exit(0);
}
else if(0==pid)
{
//子进程
while(1)
{
char buf[1024]="";
int m=read(STDIN_FILENO,buf,sizeof(buf));
write(fs[1],buf,m);
}
}
else if(pid>0)
{
//epoll监听
int nfds=epoll_wait(epollfd,change_fd,3,-1);
if(-1==nfds)
{
perror("epoll_wait");
exit(0);
}
for(int i=0;i<nfds;++i)
{
if(change_fd[i].data.fd==fd[0])
{
char buf[1024]="";
read(fd[0],buf,sizeof(buf));
printf("%s\n",buf);
}
}
}
return 0;
}
5.3.5 epoll应用于服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <stdio.h>
#include <fcnlt.h>
#include "wrap.h"
#include <sys/epoll.h>

int main(int argc,char* argv[])
{
//创建套接字,绑定
int lfd=tec4bind(8000,NULL);
//监听
Listen(lfd,128);
//创建epoll句柄
int epfd=epoll_create(128);
//将lfd上述
struct epoll_event ev,evs[1024;
ev.data.fd=lfd;
ev.events=EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
//while监听
while(1)
{
int nready=epoll_wait(epfd,evs,1024,-1);
printf("-----epoll_wait-----\n");
if(nready<0)
{
perror("epoll_wait failed\n");
break;
}
else if(nready==0)
continue;
else{
//文件描述符有变化
for(int i=0;i<nready;i++)
{
//判断lfd变化,并且时读事件变化
if(evs[i].data.fd==lfd&&evs[i].events&EPOLLIN)
{
struct sockaddr_in cliaddr;
char ip[16]="";
socklen_t len=sizeof(cliaddr);
//提取新连接
int cfd=Accepet(lfd,(struct sockaddr*)&cliaddr,&len);
printf("new connect from ip:%s port:%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
//设置cfd为非阻塞
in flags=fcnlt(cfd,F_GETFL); //获取cfd的标志位
flags|=O_NONBLOCK;
fcnlt(cfd,F_SETFL,flags);
//将cfd上树且为边缘触发
ev.data.fd=cfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
}
else if(evs[i].events&EPOLLIN){
//cdf发生变化
while(1)
{
char buf[1024]="";
int n=read(evs[i].data.fd,buf,sizeof(buf));
if(n<0)//出错
{
//若缓存区是无数据的,跳出while,继续监听
if(errno==EAGAIN)
break;
//普通错误,cfd该下树
perror("else error\n");
close(evs[i].data.fd); //关闭cfd
epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);
break;
}
else if(n==0)//客户端关闭
{
printf("client close\n");
close(evs[i].data.fd); //关闭cfd
epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);
break;
}
else{
write(STDIN_FILENO,buf,sizeof(buf));
write(evs[i].data.fd,buf,sizeof(buf));
}
}
}
}
}
}
}

6 epoll反应堆