socket 网络套接字
一个文件文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现(接收缓冲区和发送缓冲区))
通讯过程中,套接字一定是 【成对】 出现的。
小端法(PC本地存储): 高位存高地址。低位存低地址。
大端法(网络存储): 高位存低地址。低位存高地址。
涉及的四个函数:
man htonl
1.NAME
htonl, htons, ntohl, ntohs - convert values between host and network byte order
2.SYNOPSIS
#include
uint32_t htonl(uint32_t hostlong); //本地->网络(IP) converts the unsigned integer hostlong from host byte order to network byte order.
//这里的IP指的不是192.168.1.1这样的字符串形式
uint16_t htons(uint16_t hostshort); //本地->网络(Port) converts the unsigned short integer hostshort from host byte order to network byte order.
uint32_t ntohl(uint32_t netlong); //网络->本地(IP) converts the unsigned integer netlong from network byte order to host byte order.
uint16_t ntohs(uint16_t netshort); //网络->本地(Port) converts the unsigned short integer netshort from network byte order to host byte order.
主机字节序(小端)和网络字节序(大端)相互转换时,需要用到此节提到的转换函数。
1.inet_pton()//本地字节序(string IP)-> 网络字节序
//客户端connect()函数会用到
man inet_pton
1)NAME
inet_pton - convert IPv4 and IPv6 addresses from text to binary form
2)SYNOPSIS
#include
int inet_pton(int af, const char *src, void *dst);
3)PARAMETER
af:指定IP协议
AF_INET IPv4
AF_INET6 IPv6
src:传入IP地址(点分十进制,192.168.1.1)
dst:值结果参数,传出转换后的网络字节序 IP 地址。
4)RETURN VALUE
inet_pton() returns
1 on success (network address was successfully converted).
0 is returned if src does not contain a character string repre‐senting a valid network address in the specified address family. If af does not contain a valid address family,
-1 is returned and errno is set to EAFNOSUPPORT.
2.inet_ntop() //网络字节序-> 本地字节序(string IP)
//服务端accept()函数会用到
1)NAME
inet_ntop - convert IPv4 and IPv6 addresses from binary to text form
2)SYNOPSIS
#include
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
3)PARAMETER
af:指定IP协议
AF_INET IPv4
AF_INET6 IPv6
src: 传入网络字节序 IP 地址
dst:值结果参数,传出转换后的IP地址(点分十进制,192.168.1.1)。
4)RETURN VALUE
On success, inet_ntop() returns a non-null pointer to dst.
NULL is returned if there was an error, with errno set to indicate the error.
sockaddr 和 sockaddr_in 区别
大小相同,都是16字节。
区别在于:sockaddr 诞生日期早,底层封装应用的多。
sockaddr_in 后来诞生,更精细化,现在更常用。
故现在使用的都是 sockaddr_in,但在bind(),accept()时都需要将sockaddr_in强转为sockaddr才行 。
sockaddr_in结构体详解
man 7 ip //查看 sockaddr_in 信息
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
实际应用
man 7 ip //查看 sockaddr_in 信息
实际应用步骤如下:
1.定义并赋值 sockaddr_in 结构体
2.将 socketaddr_in 强转为 sockaddr 指针类型。
示例:
struct sockaddr_in addr;
addr.sin_family = AF_INET; //AF_INET6
addr.sin_port = htons(8080);
/* 方式 1,这种不常用。
inet_pton(AF_INET,"10.219.10.193",(void*)&addr_s.sin_addr);
或者
int dst;
inet_pton(AF_INET,"192.168.1.1",(void*)dst);
addr.sin_addr.s_addr = dst;
*/
//方式2 ,这是常用方式。
addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 取系统中有效的任意IP地址,二进制类型。
bind(fd,(struct sockaddr*)addr,size);
一般通讯由 3 个描述符组成。
一对用于客户端和服务器通讯,一个用于监听。
listen()函数的作用是设置监听上线(同一时刻接收到连接的个数),而不是设置监听。
accept()函数才是阻塞监听客户端连接。
accept()接收到客户端connect()连接后会生成一个新的socket用于通讯,而accept()调用的 fd 会返回继续进行监听。
客户端在connect()前没有显示bind()绑定客户端地址,那采用的就是“隐式绑定”。
socket
man 2 socket
1.NAME
socket - create an endpoint for communication
2.SYNOPSIS
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
3.PARAMETER
domain: 常用的有3个:
AF_UNIX //Local communication
AF_INET //IPv4 Internet protocols
AF_INET6 //IPv6 Internet protocols
type: 常用的有2个:
SOCK_STREAM //流 TCP
SOCK_DGRAM //报文 UDP
protocol:默认 0 即可。 //所选用协议的代表协议是什么,也就是type对应的协议。0就是默认的指代。默认:SOCK_STREAM -> TCP ;SOCK_DGRAM -> UDP.
4.RTETURN VALUE
On success, a file descriptor for the new socket is returned.
On error, -1 is returned, and errno is set appropriately.
bind
man 2 bind
1.NAME
bind - bind a name to a socket
2.SYNOPSIS
#include /* See NOTES */
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3.PARAMETER
sockfd: socket函数返回值
struct sockaddr_in addr;
addr.sinfamily = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY); //系统自行选择IP
inet_pton(AF_INET,"10.219.10.193",(void*)&addr_s.sin_addr); //指定IP
addr:(struct sockaddr *)&addr
addrlen:sizeof(addr)
4.RETURN VALUE
On success, zero is returned.
On error, -1 is returned, and errno is set appropriately.
listen
//设置同时与服务器建立连接的上线数 (同时进行三次握手的客户端数量)
man 2 listen
1.NAME
listen - listen for connections on a socket
2.SYNOPSIS
#include /* See NOTES */
#include
int listen(int sockfd, int backlog);
3.PARAMETER
sockfd: socket 函数返回值
backlog:上线数。最大值为 128
4.RETURN VALUE
On success, zero is returned.
On error, -1 is returned, and errno is set appropriately.
accept
//阻塞等待客户端连接
man 2 accept
1.NAME
accept, accept4 - accept a connection on a socket
2.SYNOPSIS
#include /* See NOTES */
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
3.PARAMETER
sockfd :socket函数返回值(也必须是listen过的fd)
addr :值结果参数。传出成功与服务器建立连接的客户端地址结构
addrlen :传入传出参数。
传入:传入时addr的大小。一般为:sizeof(addr)
传出:客户端addr的实际大小。
4.RETURN VALUE
On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket.
On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
connect
//使用创建的socket与服务器连接
man 2 connect
1.NAME
connect - initiate a connection on a socket
2.SYNOPSIS
#include /* See NOTES */
#include
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3.PARAMETER
sockfd :socket函数返回值
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&addr.sin_addr.s_addr);
addr :传入参数。服务器地址结构。(也就是要连接的服务器的地址结构) 用inet_pton()给地址赋值。
addrlen :服务器地址结构大小。
4.RETURN VALUE
If the connection or binding succeeds, zero is returned.
On error, -1 is returned, and errno is set appropriately.
注: 如果connect前未使用bind显示绑定客户端地址,采用的就是“隐式绑定”。
实现简单的服务器和客户端DEMO
DEMO:
https://github.com/Panor520/LinuxCode/tree/master/socket/tcp/simpledemo
socket
同 tcp中的socket
recvfrom
man 2 recvfrom
1.NAME
recv, recvfrom, recvmsg - receive a message from a socket
2.SYNOPSIS
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
3.PARAMETER
sockfd :通信的套接字
buf :缓冲区地址 //char buf[1024];
len :缓冲区大小 //sizeof(buf)
flags : 默认0.
src_addr:(struct addr*)&addr_c 传出,对端地址结构 //不需要对端地址信息可置 0
addrlen :传入传出参数。 socklen_t len_addr_c; //不需要对端地址信息可置 0
4.RETURN VALUE
成功 :成功接收数据字节数。
失败 :-1 errno
0 :对端关闭。
sendto
man 2 sendto
1.NAME
send, sendto, sendmsg - send a message on a socket
2.SYNOPSIS
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
3.PARAMETER
sockfd :通信的套接字
buf :缓冲区地址 //char buf[1024];
len :缓冲区大小 //sizeof(buf)
flags :默认0.
dest_addr:(struct addr*)&addr 传入,对端地址结构
addrlen :sizeof(addr).
4.RETURN VALUE
成功 :成功写如数据字节数
失败 :-1. errno
read函数返回值:
>0 :实际读到的字节数
=0 :已经读到结尾(对端已经关闭)【重!点!,必须掌握】
-1 :进一步判断errno的值
errno = EAGAIN or EWOULDBLOCK : 设置了非阻塞方式 读。没有数据到达
errno = EINTR 慢速系统调用被 中断。
errno = “其他情况” 异常。
连接地址
连接地址
先关闭服务端,主动关闭端就会经历 TIMEWAIT 状态(2MSL时长),立即再次启动服务端时会出现bind server error的错误,
可以用下面方法避免。
详见 unix网络编程 第7章
man setsockopt
先前关闭的服务端还在经历 TIMEWAIT状态,只是 端口和地址可以复用。
用法:
在服务端的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
当使用dup2()指定多个整数指向同一个文件描述符时:
close() 关闭只会关闭指定的那个。【单个关闭】
shutdown()关闭时,会将所有指向该文件描述符的连接全部关闭。【全关闭】
man 2 shutdown
1.NAME
shutdown - shut down part of a full-duplex connection
2.SYNOPSIS
#include
int shutdown(int sockfd, int how);
3.PARAMETERS
how:
SHUT_RD 关读端
SHUT_WR 关写端
SHUT_RDWR 两端都关闭
4.RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
基础
传统的 多线程和多进程服务器 就是是【阻塞】的。
而 多路I/O转接(select、poll、epoll) 是【非阻塞】的。
多路I/O转接 是由内核提供的 select、poll、epoll 监听机制。
监听connect 事件及 read、write事件。只有当 client 发生相应事件时,多路转接实现的服务器才会响应。 平时是非阻塞的(不会一直等待)。
select的实现如下图所示:
select
man 2 select
1.NAME
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
2.SYNOPSIS
/* According to POSIX.1-2001, POSIX.1-2008 */
#include
/* According to earlier standards */
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
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); //清空文件描述符集合
3.PARAMETER
nfds :监听的所有文件描述符中,最大文件描述符+1。
//例:要监听 4~50的文件描述符(47), nfds就应该填 48
readfds :读 文件描述符监听集合。 传入 传出 参数。
传入:要设置读监听的文件描述符集合
传出:发生了读事件的文件描述符集合
//例:传入集合中有4、5、6文件描述符。而4文件描述符发生了读事件,故传出的集合中只存在文件描述符4。
writefds:写 文件描述符监听集合。 传入 传出参数
传入:要设置写监听的文件描述符集合
传出:发生了写时间的文件描述符集合
//例:传入集合中有4、5、6文件描述符。而5文件描述符发生了写事件,故传出的集合中只存在文件描述符5。
exceptfds:异常 文件描述符集合。 传入 传出 参数。
传入:要设置异常监听的文件描述符集合
传出:发生了异常事件 的文件描述符集合
//例:传入集合中有4、5、6文件描述符。而6文件描述符发生了异常事件,故传出的集合中只存在文件描述符6。
timeout: >0 :设置监听时长
NULL:阻塞监听(也就是始终等待事件发生)
0 :非阻塞监听,轮询
4.fd_set
是一个位图。 0 1表示。
5.RETURN VALUE
>0 :所有监听集合(3个)中,所有传出集合的文件描述符的总数。
//例:以上面参数的例子为例,三个集合共传出了4、5、6故RETURN VALUE=3.
=0 :没有满足监听条件的文件描述符
=-1 :ERROR. errno.
select服务器DEMO思路分析:
基础select demo实现链接
升级版select demo实现链接
select | |
---|---|
缺点 | 监听上限受文件描述符限制,最大 1024。检测满足条件的fd,需自己增加业务逻辑提高效率,也是变向增加了编码难度 |
优点 | 跨平台。可在 windowns、linux、macOS、Unix、类Unix、mips 上运行 |
poll
poll 就是升级版select (增加数组记录要遍历的文件描述符)
man poll
1.NAME
poll, ppoll - wait for some event on a file descriptor
2.SYNOPSIS
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
3.PARAMETER
fds :监听的文件描述符【数组】
struct pollfd {
int fd; /* file descriptor */ //待监听的文件描述符
short events; /* requested events */ //待监听的文件描述符对应的监听事件
short revents; /* returned events */ //传入0。如果满足events给定的事件,会返回events对应的传入值。
};
events 或 revents 取值: POLLIN //读事件
POLLOUT //写事件
POLLERR //错误事件
nfds:监听数组的 实际有效监听个数。
timeout: >0 :超时时长。 单位:毫秒。
-1 :阻塞等待。
0 :不阻塞。
4.RETURN VALUE
满足对应监听事件的文件描述符 【总个数】。
poll demo实现链接
poll | |
---|---|
缺点 | 不能跨平台。只能在Linux或类Unix上运行。 不能直接定位到满足事件的文件描述符,也是变向增加了编码难度 |
优点 | 自带数组结构。可以将 监听事件集合 和 返回事件集合 分离。 可拓展 监听上线(方法同epoll)(超出1024限制) |
基础
epoll的本质是一个【红黑树】。监听结点为根节点。
epoll的使用由三个函数组成。
//epoll 应该使用非阻塞的ET模式写服务器程序(这是规则)
man epoll_create
man epoll_ctl
man epoll_wait
1.epoll_create() //创建一棵监听红黑树
1)NAME
epoll_create, epoll_create1 - open an epoll file descriptor
2)SYNOPSIS
#include
int epoll_create(int size);
3)PARAMETER
size:创建红黑树的监听结点数量 (仅供内核初始化使用,当实际使用超出该大小时,内核会自动扩容)
4)RETURN VALUE
On success, these system calls return a nonnegative file descriptor.
On error, -1 is returned, and errno is set to indicate the error.
2.epoll_ctl() //操作监听红黑树
1)NAME
epoll_ctl - control interface for an epoll file descriptor
2)SYNOPSIS
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3)PARAMETER
1.epfd :epoll_create()函数的返回值
2.op :对该监听红黑树所做的操作。有三种值:
EPOLL_CTL_ADD //添加 fd 到监听红黑树
EPOLL_CTL_MOD //修改 fd 在监听红黑树上的监听事件
EPOLL_CTL_DEL //将一个 fd 从监听红黑树上摘下(取消监听)
3.fd :待 op 操作的fd
4.event :本质为 struct epoll_event 结构体 指针。
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
成员 events 常用值:
EPOLLIN
EPOLLOUT
EPOLLERR
成员 data 原型如下:
typedef union epoll_data {
void *ptr;
int fd; //对应监听事件的fd
uint32_t u32;//不用
uint64_t u64;//不用
} epoll_data_t;
4)RETURN VALUE
When successful, epoll_ctl() returns zero.
When an error occurs, epoll_ctl() returns -1 and errno is set appropriately.
3.epoll_wait() //阻塞监听红黑树
1)NAME
epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
2)SYNOPSIS
#include
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
3)PARAMETER
1.epfd :epoll_create()函数的返回值
2.events :传出参数(数组),满足定义的监听事件的所有结点的结构体数组
3.maxevents:events 数组元素的总个数。 默认为1024 。
struct epoll_event events[1024];
4.timeout : -1 :阻塞
0 :不阻塞
-1 :失败 errno.
4)RETURN VALUE
>0 :满足监听事件的struct epoll_event结构体的总个数。可用作循环处理的上线。
0 :没有满足监听事件的struct epoll_event结构体
-1 :失败 errno
epoll |
---|
缺点 |
优点 |
基础epoll demo实现链接
epoll 事件模型
ET 模式: (服务器常用方式)
即边沿触发。
缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。新的事件满足才会触发。
设置方式:struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
注意: ET模式只支持非阻塞。需给连接的文件描述符设置读写非阻塞(利用fcntl函数),如下:
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK; //位或操作
fcntl(cfd, F_SETFL, flg);
LT 模式:
即水平触发。 ---epoll默认采用方式
缓冲区剩余未读尽的数据会导致 epoll_wait 返回。
例:通信时,readn 读数据时,每次只读 100字节,但缓冲区接收了 250 字节。
① 默认的 LT模式下 会直接 再次触发 epoll_wait()函数,表明该连接fd又有数据写过来,并再读出100字节,
之后再次触发 epoll_wait()函数,表明又有数据写过来,接收50字节后会阻塞在readn的地方,直到写满100字节。
一旦 阻塞在readn的地方,代码就会有问题,因为应该阻塞在epoll_wait()的地方。
② 在 ET模式下,剩下的150字节会被读取忽略,不会触发 epoll_wait()函数,而下次在写入数据时,会再从之前的150字节里读前100字节,后面的新写入的数据累加进来,等待下次在读取100字节。
【总结:应使用非阻塞的 ET模式写epoll服务器。】
epoll 反应堆
就是epoll基础demo的升级版,更完整些。
也是libevent 框架采用的方式。
epoll反应堆:
ET模式 + 非阻塞、轮询 + void *ptr.
简单的 epoll demo:
socket、bind、listen -- epoll_create 创建监听红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd --
-- while(1) -- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回监听满足数组。 -- 判断返回数组元素 --
-- lfd满足 -- Accept -- cfd满足 -- read -- 小--大 - 【diff content】 - write回去。
反应堆:
反应堆的优化就是在检测到对端有数据传输过来后,服务端写回时要检测,对端是否可写,确认可写再将数据写过去。
socket、bind、listen -- epoll_create 创建监听红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd --
-- while(1) -- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回监听满足数组。 -- 判断返回数组元素 --
-- lfd满足 -- Accept -- cfd满足 -- read -- 小--大 -- 【diff begin】
-- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 --epoll_ctl() -- EPOLL_CTL_ADD重新放到红黑树上监听 --
-- 等待 epoll_wait() 返回 -- 说明 cfd 可写 -- write写回去
-- cfd 从监听红黑树上摘下 -- EPOLLIN | EPOLLET -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听读事件 -- epoll_wait()监听 【diff end】
反应堆epoll demo实现链接