目录
知识点
网络协议模型
网络字节序 (很绕的概念)
什么是TCP
交换机与路由器
TCP三次握手
TCP四次挥手
TCP与UDP区别 概念 适用范围
TCP可靠性保证
TCP三次握手时产生的队列
SYN 攻击
发生RST情况
HTTP协议(超文本传输协议)
------------------------------------------------------------------------
网络编程模块
知识点
TCP编程模型
TCP三次握手
TCP四次挥手
套接字地址结构
进程与内核传递地址结构
socket()函数
connect()函数
bind()函数
listen()函数
Accept()函数
UDP编程模型
recvfrom()
sendto()
------------------------------------------------------------------------
多进程并发服务器
知识点
accept后的处理
一个服务器对多个客户端
I/O复用—select
I/O符用—epoll
模型小结
I/O小结
2020/12/28 小结
------------------------------------------------------------------------
附录
客户端编程模型
服务端编程模型
改—服务端编程模型
SELECT-服务端模型
EPOLL-服务端模型
数据链路层:
ARP(address resolve protocol)
(1)主机向自己所在的网络发送一个ARP请求,请求包含目标机器的网络地址,网络上所有其他机器都将收到这个请求,但是只有目标机器(符合网络地址)才会作出 ARP应答,应答内容包含目标机器的MAC地址。
(2)维护一个ARP高速缓存,包含 【IP -> MAC】的映射。
arp -a # 查看arp缓存内容
sudo arp -d 192.168.1.109 # 删除某个IP的mac地址
sudo arp -s 192.168.1.109 08:00:27:53:10:67 # 创建某个IP的mac地址
# 测试arp请求,须删除arp对应缓存
# laptop 执行
tcpdump -i eth0 -ent '(dst 192.168.1.109 and src 192.168.1.108) or (dst 192.168.1.108 and src 192.168.1.109)'
telnet 192.168.1.109 echo
(3)
应用层:
网络层协议:
传输层:
一种数据存储格式,与之对应的还有本机字节序,按序列内部组织方式,分成:
大端序列:高位数据存储在低位内存中。
小端序列:反过来
数据:int : 0x12345678 : 4 bytes
内存增长方向:低 -|字节|-|字节|-|字节|-|字节|-> 高
大端: 低 - 12 - 34 - 56 - 78 -> 高 (符合右手书写习惯)
小端: 低 - 78 - 56 - 34 - 12 -> 高
其中,网络字节序列属于大端序列,本机则两种都有可能。
// Linux 采用以下4种函数完成主机字节序与网络字节序之间的转换
#include
unsigned long int htol(unsigned long int hostlong); // IP
unsigned short int htos(unsigned long short hostshort); // Port
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned long short netshort);
// 查看主机序列 与 网络序列
#include
#include
int main()
{
int a = 0x12345678;
char *p = (char *)(&a);
printf("主机字节序:%0x %0x %0x %0x\n", p[0], p[1], p[2], p[3]);
int b = htonl(a); //将主机字节序转化成了网络字节序
p = (char *)(&b);
printf("网络字节序:%0x %0x %0x %0x\n", p[0], p[1], p[2], p[3]);
return 0;
}
// 主机字节序:78 56 34 12
// 网络字节序:12 34 56 78
#include
using namespace std;
bool TestEndin()
{
union // 不加类型名称,可以直接使用变量名调用
{
int numbers = 0x10000000;
char pointer;
};
// 假定写入的数据是从高位数据开始写入
if (pointer == 1) // 若从低位开始存放数据,则说明是大端模式
return true;
return false; // 否则,从高位开始存放数据,说明是小端模式
}
int main()
{
if (TestEndin())
cout << "BigEndin" <
TCP是传输控制层协议,这是一种面向连接的,提供可靠传输的协议。
面向连接:三次握手 --》 传输数据 --》 四次挥手
什么是面向连接?经过TCP三次握手之后,双方建立的资源就是所谓的链接,它不是一条真实的链路。
可靠传输:
确认机制
超时重传
流量控制
拥塞控制
交换机:主要管理MAC地址。
路由器:管理IP地址。
家里路由器:交换机(四个接口) + 路由器
目的:只建立一次链接。
TCP机制:
(1)不带数据的ACK报文不会重传,(且不消耗序列号)。
(2)双方序列号都确定后,才进行链接传输。
握手过程:
client --- | SYN = 1,seq = x | --->>> server
client <<<--- | ACK = 1,SYN = 1,seq = y,ack = x + 1| --- server
client --- | ACK = 1,seq = x + 1,ack = y + 1| --->>> server
为什么三次:
正常讲应该是:4次,分别为: SYN -> ACK -> SYN -> ACK。但是,为了调高效率,将发送的ACK报文,添加上了SYN数据,合并这两次的传输,因为变成了3次,更多参考TCP机制。
挥手过程:
client --- | FIN = 1,seq = u | --->>> server
client <<<--- | ACK = 1,seq = v,ack = u + 1 | --- server
server进入close-wait状态,并可能仍要向client发送数据
client <<<--- | FIN = 1,seq = w | --- server
client --- | ACK = 1,seq = u + 1,ack = w + 1 | --->>> server
clinet进入time-wait等待2MSL(Maximum Segment Lifetime)后,释放TCP连接
细节:
client等待2MSL(2MSL:2个最大报文段的生存时间)
(1)确保C端确认报文到达S端,若S端收到,会重新发送FIN报文。
(2)防止已经失效的请求连接报文出现在本连接中,2MSL足够让所有C端产生的报文在网络中消失。
TCP(20bytes) | UDP(8bytes) | |
---|---|---|
面向连接 | 是,需要建立连接后才能传输数据 | 否 |
可靠交付 | 是,保证可靠交付——确认、超时重传、流量控制、拥塞控制 | 否,只检验和丢失报文等处理 |
工作效率 | 低,控制多、网络开销大、系统开销大 | 高,传输控制简单、系统开销小 |
实时性 | 低 | 高 |
安全性 | 机制多,容易被利用而被攻击 | 机制少,相对安全 |
适用场景 | 要求传输质量,对实时性要求不高 | 相反 |
示例 | HTTP,HTTPS,FTP等传输文件的协议以及POP,SMTP等邮件传输协议 | 比如视频传输、实时通信等 |
传输单位 | 数据流模式 | 数据报模式 |
(1)序列号、确认机制、超时重传。
双方确认序列号后,才能进行信息传输,并根据序列号判断报文是否是需要的。
通过确认机制,来确定哪些报文到达,哪些需要重传。
(2)checksum校验和。
三部分进行校验和:TCP伪首部、TCP首部、TCP数据。
(3)流量控制。
流量窗口,指无需等待确认信号的情况下,发送端还能发送的最大数据量。
该机制设置了大量缓冲区。
(4)拥塞控制。
慢开始:从1开始,以2的倍数增加窗口值。
拥塞避免:拥塞窗口到达阈值后,每次增加1个窗口值;发生超时重传时,阈值为当前窗口一半,回到慢开始。
快重传:收到三个连续的重复确认,立即重传当前编号的下一个报文段,而不等超时重传。
快恢复:收到三个连续的重复确认,将阈值与窗口值设置为当前阈值的一半,然后执行拥塞避免。
这里每次,指的是收到一个确认报文,即为1次。
(1)半连接队列:SYN队列。
服务端接受客户端的SYN请求后,内核会将该连接存储到半连接队列中。
同时,服务器会为该连接分配TCB(传输控制块),至少要280字节,甚至1300字节。
(2)全连接队列:ACCEPT队列。
服务端接受客户端的ACK后,将连接从SYN队列中移除,然后将创建一个新的完全连接,并将其添加到accept队列,等待进程调用accept函数将连接取出。
(3)实战部分:
https://zhuanlan.zhihu.com/p/144785626 (暂时不往后看了)
(1)属于DOS攻击的一种,DOS攻击(Denial of Service,即拒接服务),另外,DDOS (Distribute)分布式攻击,指多台机器对服务器发起DOS攻击。
(2)而SYN攻击,指的是向服务器的TCP端口发送很多SYN请求,但是却不完成3次握手过程。
不完成握手的方法可以是:使用假的IP地址;
(3)攻击的是服务器的SYN队列。
(4)解决方式:
4-1、延迟分配TCB。等待正常连接建立后,再分配TCB。
4-2、SYN Cache技术,HASH表保存半连接信息,直到收到正确的回应ACK报文再分配TCB。
4-3、SYN Cookie技术:
考虑了报文中的一些固定信息,加上一些服务端的固定信息,计算出一个序列号,然后发送给客户端。
这时并不保存和分配任何信息。
接受最后一次ACK报文时,通过相同方式的计算,查看得到的结果是否时客户端发送SYN报文中的序列号。
确认则通过匹配,否则拒绝匹配。
设置以下参数
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_synack_retries = 2
(1)客户端 established 而 服务端 closed(断电重启)。
客户端发送任何信息给服务端,服务器都发回RST。
若客户端接受的到RST,则立即释放该TCP的端口号和内存资源,并使用当前窗口号,重新建立TCP连接。
(1)超文本传输协议,从服务器传输超文本到本地浏览器的协议,它基于TCP/IP,属于应用层的协议。
(1-1)HTTP协议工作在 客户端 - 服务器模型上,浏览器作为HTTP客户端,向服务端发送HTTP请求。
(2)简单快速
请求服务时,只需要传输 请求方法 和 路径,由于HTTP协议简单,使得HTTP服务器的程序规模小,通讯快速。
请求方法:GET、POST、HEAD
(3)灵活:允许传输任意类型的数据对象。
(4)无连接:含义,每次连接只处理一个请求,并在接受客户端确认后,断开连接。
(5)无状态(cookie解决该问题):对事务处理没有记忆,若后续要处理前面的信息,则必须重传,导致传输数据量大,但应答速度块。
(1)HTTP代理服务:
(1-1)正向代理:客户端,设置并发HTTP请求到正向代理服务器,由正向代理服务器返回请求的目标资源。(透明代理为正向代理的一种特殊情况)
(1-2)反向代理:服务端,存在反向代理服务器,接受HTTP请求,根据请求类型,将请求转发到内部服务器中,获取目标资源给客户端。
(1-3)开源软件:squid(均支持)、varnish(反向代理)
(2)传输过程:
IP层的源IP与目的IP一般是持久不变的,而帧头部的源MAC与目的MAC是一直变化的。
(3)请求方法:
# HTTP请求
Get http://www.baidu.com/index.html HTTP/1.0
User-Agent: Wget/1.12 (linux-gnu)
Host: www.baidu.com # 头部必须信息
Connection: close
# 空行
# 消息体
GET :申请获取资源、不对服务器造成任何影响
HEAD :类似GET,仅要求服务器头部信息,不需要任何实际内容
POST :向服务器提交数据,影响服务器(创建新资源或者更新原有资源)
PUT :上传资源
DELETE : 删除资源
TRACE :要求服务器返回原始的HTTP请求内容,可以查看中间服务器对HTTP请求的影响。
OPTIONS :查看服务器对某个特定URL都支持哪些请求方法,设置URL为*,则获得服务器所有请求方法。
CONNECT :用于某些代理服务器,它们能把请求的连接转化为一个安全隧道。
PATCH :对资源做部分修改。
注:加粗为安全方法,不对服务器产生影响。
(4)Connecton:
(值为close)-短链接:旧版HTTP,1个TCP连接只能服务1个HTTP,随后WEB服务端主动关闭TCP。
(keep-alive)-长连接:1个TCP可以处理多个HTTP,节约TCP建立时间,加快传输效率。
(5)状态码 与 状态信息:
# HTTP应答
HTTP/1.0 200 ok
Server: BWS/1.0 # 服务器名称
Content-Length: 8024
Content-Type: text/html;charset = gbk
Set-Cookie: ...
Via: 1.0 localhost (squid/3.0 STABLE18) # 所经过的所有代理服务器
# 空行
# 请求文档内容
1xx信息:100 Continue : 服务器收到请求行和头部信息,告诉客户端继续发送数据部分信息
2xx成功:200 OK : 请求成功
3xx重定向:
301 Moved Permanently : 资源被转移了,请求将被重定向
302 Found : 通知资源在其他地方可以找到,但必须以GET获取
304 Not Modified : 申请的资源没有更新,和之前获得的相同
307 Temporary Redirect : 类似302,但可以使用原始的请求方法
4xx客户端错误:
400 Bad Request : 通常客户请求错误
401 Unauthorized : 请求需要认证信息
403 Forbidden : 没有权限访问资源
404 Not Found : 资源没找到
407 Proxy Authentication Required : 客户端需要代理服务器的认证
5xx服务端错误:
500 Internet Server Error : 通常服务器错误
503 Service Unavailable : 暂时无法访问服务器
(6)Cookie
目的:保持HTTP连接状态
HTTP应答:set-Cookie - 用以标识每个客户端
HTTP请求:每个请求需要附带Cookie信息
(1)客户端编程模型 附录—客户端编程模型
(2)服务端编程模型附录—服务端编程模型
(3)使用telnet,可以直接连接服务器,配合wireshark,抓包观察连接过程。
telnet 222.201.187.181 15522
(1)由内核完成,客户端仅仅是调用了connect函数;握手过程在connect函数执行过程中完成,而服务端则是在调用accept函数执行过程中完成。
(1)双端分别调用close(fd)后,执行挥手,既可以是服务端先调用close(),也可以是客户端执行close()。
(2)客户端调用close()关闭后,服务端调用的read()随即返回0,不再阻塞。
(1)通用地址结构:
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
}
(2)IPv4地址结构:
struct in_addr
{
in_addr_t s_addr;
}
struct sockaddr_in
{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[0];
}
(3)IPv6地址结构
struct in6_addr
{
in_addr_t s6_addr[16];
}
struct sockaddr_in6
{
sa_family_t sin_family;
in_port_t sin_port;
struct in6_addr sin6_addr;
uint32_t sin6_flowinfo;
uint32_t sin6_scope_id;
}
(4)地址转换函数
inet_addr(); inet_aton(); inet_ntoa(); inet_pton(); inet_ntop();
(1)由于数据经过层层封装后,最后通过网卡发送出去,而网卡等硬件设备的控制权在内核手上,因此必定会存在进程数据结构传递到内核的阶段。同样,接收数据后,也由内核先解包后传递到进程空间。
(2)进程 --> 内核:bind、connect、sendto
(3)内核 --> 进程:accept、recvfrom、getsockname、getpeername
// 1、通用socket地址
#include
struct sockaddr // 表示sock地址的结构体
{
sa_family_t sa_family; // 地址族类型变量 对应 协议族类型
char sa_data[14]; // 存放socket的地址值
}
/*
常见协议族:
| 协议族 | 地址族 | 描述 |
| PF_UNIX | AF_UNIX | UNIX本地域协议族 |
| PF_INET | AF_INET | TCP/IPv4 协议族 |
| PF_INET6| AF_INET6| TCP/IPv6 协议族 |
*/
// 2、通用socket地址2
略
// 3、专用socket地址
// 3-1、UNIX本地域协议族
#include
struct sockaddr_un
{
sa_family_t sin_family; // 地址族:AF_UNIX
char sun_path[108]; // 文件地址名
};
// 3-2、TCP/IP
struct sockaddr_in
{
sa_family_t sin_family; // 地址族:AF_INET
u_int16_t sin_port; // 端口号,要使用网络字节序
struct in_addr sin_addr; // IPv4地址结构体
};
struct in_addr
{
u_int32_t s_addr; // IPv4地址
};
// IP地址转换函数
#include
in_addr_t inet_addr(const char *strptr); // 字符串转网络字节序整数
int inet_aton(const char *cp, struct in_addr* inp); // 同上,但错误存储在inp指向的地址
char * inet_ntoa(struct in_addr in); // 网络字节序整数转换成字符串 --> 此代码不可重入
// 还有
int inet_pton(int af, const char* src, void* dst);
const char* inet_ntop(int af, const char* src, char * dst, socklen_t cnt);
int socket(int domain, int type, int protocol);
int domain : AF_INET IPv4协议 | AF_INET6 IPv6协议 | AF_LOCAL/AF_UNIX 本地套接字 | AF_PACKET
int type : SOCK_STREAM 字节流 | SOCK_DGRAM 数据报 | SOCK_RAW 原始套接字
int protocol : --
(1)没有与之相对应的端口发生连接,hardware error,客户端收到RST,返回ECONNREFUSED。
(2)发送请求时IP不可达(no route to host),协议ICMP,software error,通常是发送arp请求没有响应。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 发送服务端地址给内核
// IP地址可以使用通配地址,端口号0表示由内核分配临时端口
// INADDR_ANY,表示由内核选择IP,可以选择主机任意IP
// 分配的临时端口,使用getsockname来返回临时端口
// 通常错误:Address already in use
// 使用套接字选项来设置:setsockopt | SO_REUSEADDR
int listen(int sockfd, int backlog);
// backlog 的理解
// 内核会维护两个队列:
// 1. 半连接队列:客户端发送了SYN,被服务端接受后,就会进入半连接队列。
// 2. 全连接队列:三次握手完成后,半连接队列的结点将进入全完成队列。
// 从Listen监听队列中获取一个连接,而不关心其状态。
(1)客户端:socket() --> sendto() --> recvfrom --> close()
(2)服务端:socket() --> bind() --> recvfrom() --> sendto() -->close()
(3)服务端通过recvfrom()获知客户端的地址,然后sendto()进行消息交互。
(4)客户端首先发送sendto()给服务端,然后通过recvfrom()获取消息。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags
struct sockaddr *src_addr, socklen_t *addrlen);
// 从内核中获取 buf数据、源地址和长度
ssize_t sendto(int sockfd, void *buf, size_t len, int flags
struct sockaddr *dest_addr, socklen_t addrlen);
// 向内核传输 buf数据、目标地址和长度
(1)多进程:在接收通信套接字后,使用fork()创建子进程。
(2)子进程必须先关闭 监听套接字,因为子进程复制时,套接字引用计数+1,这么做是为了避免套接字无法关闭。
(3)父进程也需要关闭 通信套接字。
(4)创建listen的过程,将其封装,如改—服务端编程模型
(5)另外,需要注意子进程先结束时,要避免其成为僵尸进程。
(1)服务端可以使用多进程的模式,对客户端请求做出响应。
(2)进行多次交互时,记得清空buf,以免发生意想不到的错误。
(1)修改了服务端模型,使用I/O符用来代替多进程模型。SELECT-服务端模型
(2)很多细节:
(2-1)FD_ZERO(&global_rdfs); // 全局文件描述符置0;重置
(2-2)FD_SET(listen_fd, &global_rdfs); // 设定想要关注的文件描述符
(2-3)select(max_fd + 1, ¤t_rdfs, NULL, NULL, NULL) // 注意第一个参数 ==》 最大文件描述符 + 1
(2-4)for (int i = 0; i <= max_fd; ++i) // 循环查看是哪个描述符,产生了新的内容,注意大于等于
(2-5) if ( FD_ISSET(i, ¤t_rdfs) ) // 查看是否是第 i 个描述符产生了新内容
(3)select 最大描述符数量 --> 1024(2048,看系统),太大容易给客户端造成延时的现象。
用户数量 ≈ 1000
(4)缓冲区的数据没有读完,select仍然会有提示有数据产生。
(5)poll 与select 的区别在于,poll使用链表来保存fd,可以存的fd更多。
(1)EPOLL-服务端模型,~容错处理没有完善。
(2)可以使用的量
(3)epoll编程模型框架
// 1. 创建 epoll,MAX_EVENTS 指定 epoll 容纳的文件描述符数量
epoll_fd = epoll_create(MAX_EVENTS);
// 2. 设置 监听描述符为非阻塞
fcntl(listen_fd, F_SETFL, O_NONBLOCK);
// 3. 添加 监听描述符 为epoll 关注对象
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &tmp_ev);
// 4. 循环中,获取有相应的文件描述符
while (1)
{
fd_nums = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
// events 事件列表,用于存储触发事件的相关信息
...
}
// 5. 循环遍历所有文件描述符
for (int i = 0; i < fd_nums; ++i)
{
// 6.获取对应文件描述符信息,并进行操作。
if (events[i].data.fd == listen_fd)
{
// 有客户端连接,添加到epoll中
tmp_ev.data.fd = sock_fd; tmp_ev.events = EPOLLIN | EPOLLET; // ET边缘触发
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &tmp_ev);
}
else
{
// 传递fd,随后处理数据
process_data(events[i].data.fd);
// 若要关闭连接
// 删除对文件描述符的关注
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &tmp_ev);
close(events[i].data.fd);
}
}
(4)LT(Level Triggered) 与 ET(Edge Triggered)
LT :默认模式,select, poll 也采用这种模式,当缓冲还有数据没有读完时,内核会继续通知服务器。
ET :高速模式,缓冲有数据时,内核只通知服务器1次,不管缓冲数据是否被读完。
(1)PPC(process per connection) | TPC (thread per connection)
(2)select | poll
(3)epoll
bio, 阻塞io
多线程,切换开销大
nio,非阻塞方式询问io
多路复用,select/poll,循环访问fd,且有限个数,但可以修改,ulimited。
epoll,网卡进入信息后,结合事件+中断,DMA等技术,不需要循环遍历,直接返回有响应的fd。
(1)select
1-1、FD上限1024。
1-2、重复初始化:每次调用select(),都需要将fd集合从用户态拷贝到内核态。
1-3、内核遍历集合:逐个排查FD,查看时候有消息产生。
1-4、外面也需要循环遍历:查看是哪个FD产生了消息。
(2)poll
2-1、取消FD上限为1024。
(3)epoll (仅支持linux)
3-1、取消FD上限为1024
3-2、使用epoll_ctl()注册FD,用户态到内核态只需要1次拷贝。
3-3、epoll_wait只关心“就绪”的文件描述符,而不需要遍历所有FD。
通过设定FD的回调函数,将就绪的FD加入到就绪队列中。
(4)select、poll和epoll 小结
4-1、并非选择epoll最好,因为回调函数也需要消耗。
4-2、若FD较少或者(FD很多且活跃),三种都可以用。
4-3、出现较多空连接或者死链接,可以使用epoll。
(5)信号驱动I/O
设定信号处理函数,接受成功信号后,通过信号处理函数来完成后续的操作。(工程少用)
不是异步,因为接受数据(即从内核空间到用户空间这段,需要阻塞)
(6)异步I/O (linux下,支持两种异步I/O)
#include
#include
#include
#include
#include
#include
#include
const int MAX_BUFFER_SIZE = 1024;
// 处理错误函数
void handdle_error (char * msg)
{
perror(msg);
abort();
}
int main(int argc, char * argv[])
{
struct sockaddr_in server_addr;
// 创建套接字文件描述符
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0)
handdle_error(const_cast("socket failed"));
// 设定服务端IP及端口号
bzero(&server_addr, sizeof(server_addr)); // memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(15522);
// 连接服务端
if (connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) // sizeof(server_addr) 的长度是分了区别使用的协议
handdle_error(const_cast("connect failed"));
// 读取套接字文件描述符
char buffer[MAX_BUFFER_SIZE];
bzero(buffer, sizeof(buffer)); // 若不将buffer置0,输出将出现乱码
int bytes = read(socket_fd, buffer, MAX_BUFFER_SIZE); // 表示最大读取 MAX_BUFFER_SIZE个字节
if (bytes < 0)
handdle_error(const_cast("read failed"));
if (0 == bytes)
handdle_error(const_cast("read 0 bytes"));
// 客户端本地显示信息
printf("Receive bytes: %d\n", bytes);
printf("Time : %s\n", buffer);
// 关闭套接字文件描述符
close(socket_fd);
}
#include
#include
#include
#include
#include
#include
#include
#include
const int MAX_BUFFER_SIZE = 1024;
const int MAX_LISTEN_QUE = 5;
// 错误处理函数
void handdle_error (char * msg)
{
perror(msg);
abort();
}
int main (int argc, char * argv[])
{
struct sockaddr_in server_addr, client_addr;
// 监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0)
handdle_error(const_cast("Listen_fd failed"));
// 设置端口地址可重用
int opt;
if ((setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) < 0)
handdle_error(const_cast("reset socket reuse falied"));
// 设定本机地址
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 若有多个IP地址,均用
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(15522);
// 绑定当前主机的套接字地址
if (bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(struct sockaddr)) < 0)
handdle_error(const_cast("Bind server failed"));
// 监听
listen(listen_fd, MAX_LISTEN_QUE); // 创建监听队列,并赋值长度
char buffer[MAX_BUFFER_SIZE];
time_t real_time;
while (1)
{
socklen_t len;
// 接受端口输入
int socket_fd = accept(listen_fd, (struct sockaddr*) &client_addr, &len); // 获取客户端信息及结构体长度
if (socket_fd < 0)
handdle_error(const_cast("accept socket_fd failed"));
// 写入信息
real_time = time(nullptr);
snprintf(buffer, sizeof(buffer), "%s", ctime(&real_time)); // 把时间字符串写入缓冲中,然后写入套接字文件描述符
write(socket_fd, buffer, strlen(buffer)); // 控制写入字符个数
close(socket_fd); // 必须关闭,不然占用文件描述符个数
}
close(listen_fd);
}
#include
#include
#include
#include
#include
#include
#include
#include
const int MAX_QUEUE = 5;
const int MAX_BUFER_SIZE = 100;
// 错误处理函数
void handdle_error (char * msg)
{
perror(msg);
abort();
}
// 建立监听,获取监听文件描述符
int listen_and_get_sockfd()
{
int listen_fd, opt = 1;
sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(15520);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if ( (listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
handdle_error(const_cast("create listen socket error"));
if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
handdle_error(const_cast("setsockopt error"));
if ( bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(struct sockaddr)) < 0)
handdle_error(const_cast("bind error"));
listen(listen_fd, MAX_QUEUE);
return listen_fd;
}
int main(int argc, char * argv[])
{
int listen_fd, sock_fd;
socklen_t len;
sockaddr_in client_addr;
char buf[MAX_BUFER_SIZE];
time_t real_time;
bzero(buf, sizeof(buf));
listen_fd = listen_and_get_sockfd();
while (1)
{
if ( (sock_fd = accept(listen_fd, (sockaddr *)&client_addr, &len)) < 0)
handdle_error( const_cast("create socket error") );
real_time = time(nullptr);
snprintf(buf, sizeof(buf), "%s", ctime(&real_time));
write(sock_fd, buf, strlen(buf));
close(sock_fd);
}
close(listen_fd);
}
#include
#include
#include
#include
#include
#include
#include
#include
const short PORT = 15521;
const int MAX_LISTEN_QUEUE = 5;
const int MAX_BUFFER_SIZE = 1024;
void handdle_error(char * msg)
{
perror(msg);
abort();
}
int listen_and_get_sockfd()
{
int listen_fd, opt = 1;
sockaddr_in server_addr;
socklen_t len = sizeof(sockaddr_in);
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if ( ( listen_fd = socket(AF_INET, SOCK_STREAM, 0) ) < 0 )
{
handdle_error(const_cast ("create listen_fd falied"));
}
if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
handdle_error(const_cast("setsockopt error"));
}
if ( bind(listen_fd, (sockaddr*) &server_addr, len) < 0 )
{
handdle_error(const_cast ("bind failed"));
}
if ( listen(listen_fd, MAX_LISTEN_QUEUE) < 0)
{
handdle_error(const_cast ("listen failed"));
}
return listen_fd;
}
int main()
{
int sock_fd, max_fd = 0;
sockaddr_in client;
socklen_t len = sizeof(sockaddr_in);
char buf[MAX_BUFFER_SIZE];
fd_set global_rdfs, current_rdfs;
bzero(&client, sizeof(client));
int listen_fd = listen_and_get_sockfd();
FD_ZERO(&global_rdfs); // 全局文件描述符置0;重置
FD_SET(listen_fd, &global_rdfs); // 设定想要关注的文件描述符
max_fd = max_fd > listen_fd? max_fd : listen_fd; // 记录最大的文件描述符的数量
while (1)
{
current_rdfs = global_rdfs; // 重置
if( select(max_fd + 1, ¤t_rdfs, NULL, NULL, NULL) < 0)
{
// 注意第一个参数 ==》 最大文件描述符 + 1 ,易错点
// 阻塞监听max_fd个文件描述符,与recv阻塞监听的效果类似,不同在于这里监听的对象不止一个
// 后面三个NULL,对应 写文件描述符集,异常文件描述符集,阻塞
handdle_error(const_cast ("select failed"));
}
for (int i = 0; i <= max_fd; ++i) // 循环查看是哪个描述符,产生了新的内容
{
if ( FD_ISSET(i, ¤t_rdfs) ) // 查看是否是第 i 个描述符产生了新内容
{
if (i == listen_fd) // 如果是描述符是 监听描述符
{
if( (sock_fd = accept(listen_fd, (sockaddr*) &client, &len)) < 0)
{
handdle_error(const_cast ("accpet failed"));
}
FD_CLR(i, ¤t_rdfs); // 清除这部分产生的内容信息,防止下次信号。
max_fd = max_fd > sock_fd? max_fd : sock_fd; // 查看文件描述符是否增加
FD_SET(sock_fd, &global_rdfs); // 加入到关注文件描述列表
printf("client in : %d ", sock_fd);
}
else // 其它文件描述符
{
// printf("read socket : %s", i )
bzero(buf, sizeof(buf));
int bytes = recv(i, buf, MAX_BUFFER_SIZE, 0);
if (bytes < 0)
{
handdle_error(const_cast ("recv failed"));
}
if (bytes == 0) // 接受FIN信号
{
FD_CLR(i, &global_rdfs); // 取消关注该文件描述符号
close(i);
continue;
}
printf("buf : %s\n", buf);
send(i, buf, strlen(buf), 0);
}
}
}
}
}
/*
BUG 日志:
1. 轮询fd的时候,由于 < max_fd,导致一直没有结果出现,加上<=后,正常工作。
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
const short PORT = 15521;
const int MAX_LISTEN_QUEUE = 5;
const int MAX_BUFFER_SIZE = 1024;
const int MAX_EVENTS = 100;
const int ERR = -1;
const int EXIT = -2;
const int OK = 0;
void handdle_error(char * msg)
{
perror(msg);
abort();
}
int listen_and_get_sockfd()
{
int listen_fd, opt = 1;
sockaddr_in server_addr;
socklen_t len = sizeof(sockaddr_in);
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if ( ( listen_fd = socket(AF_INET, SOCK_STREAM, 0) ) < 0 )
{
handdle_error(const_cast ("create listen_fd falied"));
}
if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
handdle_error(const_cast("setsockopt error"));
}
if ( bind(listen_fd, (sockaddr*) &server_addr, len) < 0 )
{
handdle_error(const_cast ("bind failed"));
}
if ( listen(listen_fd, MAX_LISTEN_QUEUE) < 0)
{
handdle_error(const_cast ("listen failed"));
}
return listen_fd;
}
int process_data(int fd)
{
char buf[MAX_BUFFER_SIZE];
int bytes;
bytes = recv(fd, buf, MAX_BUFFER_SIZE, 0);
if (bytes < 0)
{
perror("recv error");
return ERR;
}
if (bytes == 0)
{
return EXIT;
}
printf("buf : %s", buf);
send(fd, buf, bytes, 0);
return OK;
}
int main()
{
int sock_fd, fds, tmp_return_value;
sockaddr_in client;
socklen_t len = sizeof(sockaddr_in);
struct epoll_event tmp_ev, events[MAX_EVENTS]; // 创建时间的临时对象
bzero(&client, sizeof(client));
int epoll_fd = epoll_create(MAX_EVENTS); // 创建 epoll,指定epoll容纳的文件描述符数量
if (epoll_fd < 0)
{
handdle_error(const_cast("create epoll error"));
}
int listen_fd = listen_and_get_sockfd();
fcntl(listen_fd, F_SETFL, O_NONBLOCK); // 设置监听文件描述为 非阻塞模式
tmp_ev.data.fd = listen_fd; // 设置文件描述符
tmp_ev.events = EPOLLIN; // 设置事件的类型 EPOLLIN 表示有数据进来,可以读取
tmp_return_value = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &tmp_ev); // 将文件描述符及其对应的事件,添加到epoll中,予以关注
if (tmp_return_value < 0)
{
handdle_error(const_cast("control epoll error"));
}
while (1)
{
// 检测超时 time_out
fds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 返回有相应的文件描述符
// 表示事件,-1为永远等待,
if (fds < 0)
{
handdle_error(const_cast ("epoll wait error"));
}
for (int i = 0; i < fds; ++i)
{
if (events[i].data.fd == listen_fd)
{
sock_fd = accept(listen_fd, (sockaddr*) &client, &len);
if (sock_fd < 0)
{
handdle_error(const_cast ("accept error"));
}
tmp_ev.data.fd = sock_fd;
tmp_ev.events = EPOLLIN | EPOLLET; // Edge trigger 边缘触发模式
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &tmp_ev); // 添加到关注列表中
}
else
{
tmp_return_value = process_data(events[i].data.fd); // 处理数据
if (tmp_return_value == EXIT)
{
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &tmp_ev); // 删除对文件描述符的关注
close(events[i].data.fd); // 关闭文件描述符
}
}
}
}
}