day15
本章目标
UDP聊天室
未完成,先欠着,打算和Qt的一个项目做在一起。
epollsrv.cpp
文件源代码,epoll
只是服务器发生了变化而客户端不需要变化。
1 | #include <stdio.h> |
TCP
客户/服务器模型 —> C/S
模型echosrv.c
echocli.c
文件man 2 socket 查看帮助
包含头文件<sys/socket.h>
功能:创建一个套接字用于通信
原型:
int socket(int domain, int type, int protocol);
参数:
domain
:指通信协议族(protocol family)
type
:指定socket
类型。SOCK_STREAM
;SOCK_DGRAM
;SOCK_RAW
;protocol
:协议类型返回值:
成功返回非负整数,它和文件描述符类似,我们把它称为套接字,简称套接字;
失败返回
-1
。
包含头文件<sys/socket.h>
功能:绑定一个本地地址到套接字
原型
ing bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
参数
sockfd
:sockfd
函数返回的套接字addr
:要绑定的地址addrlen
:地址长度返回值:
成功返回
0
,失败返回-1
。
man 2 listen
SOMAXCONN
查看此宏,是第二个参数,表示队列的最大值
<sys/socket.h>
int listen(in sockfd, int backlog);
sockfd
:socket
函数返回的套接字backlog
:规定内核为此套接字排队的最大连接个数0
,失败返回-1
。主动套接字:主动发起连接,会调用
connect()
函数。被动套接字:用来接收连接的,会调用
accept()
函数。
listen
函数应该在调用socket
和bind
函数之后,调用函数accept
之前调用<sys/socket.h>
int accept(int socket, struct sockaddr * addr, socklen_t * addrlen);
sockfd
:服务器套接字addr
:将返回对等方的套接字地址addrlen
:返回对等方的套接字地址长度。是一个输入输出的参数,所以一定要初始化。-1
.<sys/socket.h>
addr
所指定的套接字int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
sockfd
:未连接套接字addr
:要连接的套接字地址adrlen
:第二个参数addr
长度0
,失败返回-1
。socket
?
socket
可以看成是用户进程与内核网络协议栈的编程接口;
socket
不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程通信。套接口通信是全双工通信。
socket
可以异构通信,通信双方的软件、硬件可以不同。
IPv4套接口地址结构通常也称为”网际套接字地址结构”,它以”sockaddr_in”命名,定义在头文件<netinet/in.h>中****
man 7 ip
查看sockaddr_in
结构体,sockaddr_in
仅仅应用于IPv4
地址结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 struct sockaddr_in
{
> uint8_t sin_len;
> sa_family_t sin_family;
> in_port_t sin_port;
> struct in_addr sin_addr;
> char sin_zero[8];
>}
sin_len
:整个sockaddr_in
结构体的长度,在4.3BSD-Reno版本之前的第一个成员是sin_family
;
sin_family
:指定该地址家族,在这里必须设为AF_INET
;
sin_port
:端口;
sin_addr
:IPv4的地址;
sin_zero
:暂不使用,一般将其设置为0。
通用地址结构用来指定套接字关联的地址,
sockaddr
可以应用于所有的协议的套接口
1
2
3
4
5
6
7
8
9
10
11 >struct sockaddr
>{
> uint8_t sin_len;
> sa_family_t sin_family;
> char sa_data[14];
>}
sin_len
:整个sockaddr
结构体的长度;
sin_family
:指定该地址家族;
sa_data
:由sin_family
决定它的形式。
字节序
大端字节序(Big Endian)
最高有效位(MSB: Most Significant Bit)存储于最低内存地址处,最低有效位(LSB: Lowest Significant Bit)存储于最高内存地址处。
小端字节序(Little Endian)
最高有效位(MSB:Most Significant Bit)存储于最高内存地址处,最低有效位(LSB: Lowest Significant Bit)存储于最低内存地址处。
假设有字节序0x12345678
内存地址 | 低地址 | –> | –> | 高地址 |
---|---|---|---|---|
大端字节序(0x) | 12 | 34 | 56 | 78 |
小端字节序(0x) | 78 | 56 | 34 | 12 |
主机字节序
不同的主机有不同的字节序,如
x86
为小端字节序,Motorola 6800
为大端字节序,ARM
字节序是可配置的。
网络字节序
网络字节序规定为大端字节序。
计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
man 3 byteorder 查询字节序转换函数
1 | #include <arpa/inet.h> |
man 3 inet
查询地址转换函数
1 | #include <sys/socket.h> |
流式套接字(SOCK_STREAM) —> TCP/IP协议
提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
数据报式套接字(SOCK_DGRAM)
提供无连接服务,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。
原始套接字(SOCK_RAW)
跨越传输层,直接对
IP
层进行数据封装的套接字,通过原始套接字,将应用层的数据封装成IP
层能够认识的套接字。
REUSEADDR
,地址重复利用process-per-conection
)REUSEADDR
。setsockopt
来设置REUSEADDR
套接字选项。REUSEADDR
选项可以使得不必等待TIME——WAIT
状态消失就可以重启服务器。
getppid()
获取父进程的pid
1 | #include <stdio.h> |
1 | #include <stdio.h> |
read
、write
与recv
、send
readline
实现readline
实现回射客户端/服务器getsockname
、getpeername
gethostname
、gethostbyname
、gethostbyadr
man recv
recv
函数只能用于套接口的I/O·
,read
函数可以用于任何的I/O
recv
函数增加了可选参数flgs
,flags取值为MSG_OOB
:接收紧急指针数据。MSG_PEEK
:接收数据但是不把缓冲区的数据清除。
1 | #include <sys/types.h> |
##flags标志:
1 | MSG_OOB |
readline
:是为了解决粘包问题。
getsockname
:获取套接口本地的地址
getpeername
:获取远程套接口的地址
1 | #include <sys/socket.h> |
1 | #include <sys/socket.h> |
gethostname
:获取主机名字
gethostbyname
:通过主机名获取所有的IP地址
1 | #include <unistd.h> |
1 | #include <netdb.h> |
注:
struct hostent
的成员h_addr_list
虽然是char **
但是是需要用inet_ntoa
转化一下才能输出IP地址。
参考链接:
https://www.cnblogs.com/yuxingfirst/p/3165407.html
TCP
回射客户/服务器
TCP
是个流协议
僵尸进程与SIGCHLD
信号
TCP
是基于字节流传输的,只维护发送出去多少,确认收到了多少,没有维护消息与消息之间的边界,因而可能导致粘包问题,所以需要自己维护。\n
作为区分消息和消息的边界僵尸进程:子进程死后,父进程未给子进程收尸,即父进程未回收子进程的资源
孤儿进程:父进程先于子进程死亡,子进程没有了父进程,成为了孤儿进程,而
linux
操作系统有一个孤儿院,进程ID为1的进程
,会成为孤儿进程的父进程,使孤儿进程的资源可以让父进程回收
1 | [pip@localhost code]$ ps -ef | grep echosrv |
第三行即为僵尸进程。
SIGCHLD
信号1 | singal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信号 |
wait()
函数1 | #include <sys/types.h> |
pid_t wait(int *status);
获取子进程的状态,传入NULL
即为不关心子进程状态。
第一种处理结果
1 | pip 4213 4145 0 17:40 pts/1 00:00:01 vim echosrv.c |
第二种处理结果
1 | pip 4213 4145 0 17:40 pts/1 00:00:01 vim echosrv.c |
由上两种处理结果发现僵尸进程都不存在了,则使用``SIGCHLD`可以避免僵尸进程的出现。
如果有多个子进程那么就会出现问题,
wait
只会处理第一个返回的子进程,然后返回,如果需要处理多个子进程需要使用waitpid
等待指定的只进程返回。
如果有多个子进程的话,上述两种方式都可能会产生僵尸进程,相信如果是第一种情况对于多个子进程处理是肯定有僵尸进程的,因为
wait
只等待一个子进程就退出了,而虽然waitpid
可以等待多个子进程,但是如果子进程同时发送SIGCHLD
信号,那么父进程可能不能接收到子进程的SIGCHLD
信号,导致不能对子进程的资源进行回收。使子进程成为了僵尸进程。
更加好的回收的子进程资源的方式出现了
1 | void signal_handle(int sign) |
RETURN VALUE//返回值
wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.
waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. On error, -1 is returned.
TCP
11种状态TIME_WAIT
与SO_REUSEADDR
SIGPIPE
参考文档:
https://zhidao.baidu.com/question/27114298.html
ACK
:是一种确认应答,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。SYN
:攻击属于DOS
攻击的一种,它利用TCP协议
缺陷,通过发送大量的半连接请求
,耗费CPU和内存资源。是最常见又最容易被利用的一种攻击手法。FIN
:是用来扫描保留的端口,发送一个FIN
包(或者是任何没有ACK或SYN标记的包)到目标的一个开放的端口,然后等待回应。许多系统会返回一个复位标记。
貌似有点不对啊,不是说是11种状态吗???怎么只有10种,莫非打错字了???NO!!!NO!!!NO!!!
第十一种状态为:
CLOSING
关闭请求由双方都可以主动的发起,图中为客户端发起的中断连接,也可以是服务器发起中断连接。
TIME_WAIT
:保留两倍的MSL
时间。参考:MSL,即 Maximum Segment Lifetime,一个数据分片(报文)在网络中能够生存的最长时间,在RFC793中定义MSL通常为2分钟,即超过两分钟即认为这个报文已经在网络中被丢弃了。对于一个TCP连接,在双方进入TIME_WAIT后,通常会等待2倍MSL时间后,再关闭掉连接,作用是为了防止由于FIN报文丢包,对端重发导致与后续的TCP连接请求产生顺序混乱,具体原理这里就不详细解释了,
参考链接:
https://blog.csdn.net/qwertyupoiuytr/article/details/71436967
如果客户端和服务器双方同时关闭
close
时,会产生此状态,建议参考连接。参考文档:
1 | [pip@localhost code]$ netstat -an | grep tcp | grep 6000 |
往一个已经接收FIN
的套接字种写是允许的,接收到FIN
仅仅代表对方不再发送数据
在收到RST
段之后,如果再调用write
就会产生SIGNPIPE
信号,对于这个信号的处理,我们通常忽略即可
signal(SIGPIPE, SIG_IGN);
参考文档: