linux网络编程-tcp通信
socket编程
网络套接字
网络套接字: socket
- 一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)
- 在通信过程中, 套接字一定是成对出现的。
前置知识
网络字节序:
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678
大端法:(网络存储) 高位存低地址。地位存高地址。
htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
htons --> 本地--》网络 (port)
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port)
IP地址转换函数:
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序
af:AF_INET、AF_INET6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值: 成功:dst。
失败:NULL
sockaddr地址结构:
IP + port --> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 man 7 ip
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。
bind(fd, (struct sockaddr *)&addr, size);
socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol); 创建一个 套接字
domain:AF_INET、AF_INET6、AF_UNIX
type:SOCK_STREAM、SOCK_DGRAM
protocol: 0
返回值:
成功: 新套接字所对应文件描述符
失败: -1 errno
bind
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port)
sockfd: socket 函数返回值
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr: 传入参数(struct sockaddr *)&addr
addrlen: sizeof(addr) 地址结构的大小。
返回值:
成功:0
失败:-1 errno
listen
int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
sockfd: socket 函数返回值
backlog:上限数值。最大值 128.
返回值:
成功:0
失败:-1 errno
accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
sockfd: socket 函数返回值
addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
socklen_t clit_addr_len = sizeof(addr);
addrlen:传入传出。 &clit_addr_len
入:addr的大小。 出:客户端addr实际大小。
返回值:
成功:能与客户端进行数据通信的 socket 对应的文件描述。
失败: -1 , errno
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接
sockfd: socket 函数返回值
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
TCP通信流程分析:
server:
-
socket() 创建socket
-
bind() 绑定服务器地址结构
-
listen() 设置监听上限
-
accept() 阻塞监听客户端连接
-
read(fd) 读socket获取客户端数据
-
小--大写 toupper()
-
write(fd)
-
close();
client:
-
socket() 创建socket
-
connect(); 与服务器建立连接
-
write() 写数据到 socket
-
read() 读转换后的数据。
-
显示读取结果
-
close()
多进程并发服务器
多进程并发服务器:server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {
cfd = Accpet(); 接收客户端连接请求。
pid = fork();
if (pid == 0){ 子进程 read(cfd) --- 小-》大 --- write(cfd)
close(lfd) 关闭用于建立连接的套接字 lfd
read()
小--大
write()
} else if (pid > 0) {
close(cfd); 关闭用于与客户端通信的套接字 cfd
contiue;
}
}
5. 子进程:
close(lfd)
read()
小--大
write()
父进程:
close(cfd);
注册信号捕捉函数: SIGCHLD
在回调函数中, 完成子进程回收
while (waitpid());
多线程并发服务器
多线程并发服务器: server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {
cfd = Accept(lfd, );
pthread_create(&tid, NULL, tfn, (void *)cfd);
pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。
}
5. 子线程:
void *tfn(void *arg)
{
// close(lfd) 不能关闭。 主线程要使用lfd
read(cfd)
小--大
write(cfd)
pthread_exit((void *)10);
}
多路IO转接
select:
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
FD_ZERO
void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。
fd_set rset;
FD_ZERO(&rset);
FD_SET
void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);
FD_CLR
void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。
FD_CLR(4, &rset);
FD_ISSET
int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。
返回值: 在:1;不在:0;
FD_ISSET(4, &rset);
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
思路分析:
int maxfd = 0;
lfd = socket() ; 创建套接字
maxfd = lfd;
bind(); 绑定地址结构
listen(); 设置监听上限
fd_set rset, allset; 创建r监听集合
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
while(1) {
rset = allset; 保存监听集合
ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。
if(ret > 0) { 有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。
cfd = accept(); 建立连接,返回用于通信的文件描述符
maxfd = cfd;
FD_SET(cfd, &allset); 添加到监听通信描述符集合中。
}
for (i = lfd+1; i <= 最大文件描述符; i++){
FD_ISSET(i, &rset) 有read、write事件
read()
小 -- 大
write();
}
}
}
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】
struct pollfd {
int fd: 待监听的文件描述符
short events: 待监听的文件描述符对应的监听事件
取值:POLLIN、POLLOUT、POLLERR
short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
}
nfds: 监听数组的,实际有效监听个数。
timeout: > 0: 超时时长。单位:毫秒。
-1: 阻塞等待
0: 不阻塞
返回值:返回满足对应监听事件的文件描述符 总个数。
优缺点:
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 当前计算机所能打开的最大文件个数。 受硬件影响。
ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024
修改:
打开 sudo vi /etc/security/limits.conf, 写入:
* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】
* hard nofile 100000 --> 命令修改上限。
epoll:
epoll_create
int epoll_create(int size); 创建一棵监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考。)
返回值:指向新创建的红黑树的根节点的 fd。
失败: -1 errno
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树
epfd:epoll_create 函数的返回值。 epfd
op:对该监听红黑数所做的操作。
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd:
待监听的fd
event: 本质 struct epoll_event 结构体 地址
成员 events:
EPOLLIN / EPOLLOUT / EPOLLERR
成员 data: 联合体(共用体):
int fd; 对应监听事件的 fd
void *ptr;
uint32_t u32;
uint64_t u64;
返回值:成功 0; 失败: -1 errno
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。
epfd:epoll_create 函数的返回值。 epfd
events:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体。
maxevents:数组 元素的总个数。 1024
struct epoll_event evnets[1024]
timeout:
-1: 阻塞
0: 不阻塞
>0: 超时时间 (毫秒)
返回值:
> 0: 满足监听的 总个数。 可以用作循环上限。
0: 没有fd满足监听事件
-1:失败。 errno
epoll实现多路IO转接思路:
lfd = socket(); 监听连接事件lfd
bind();
listen();
int epfd = epoll_create(1024); epfd, 监听红黑树的树根。
struct epoll_event tep, ep[1024]; tep, 用来设置单个fd属性, ep 是 epoll_wait() 传出的满足监听事件的数组。
tep.events = EPOLLIN; 初始化 lfd的监听属性。
tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 将 lfd 添加到监听红黑树上。
while (1) {
ret = epoll_wait(epfd, ep,1024, -1); 实施监听
for (i = 0; i < ret; i++) {
if (ep[i].data.fd == lfd) { // lfd 满足读事件,有新的客户端发起连接请求
cfd = Accept();
tep.events = EPOLLIN; 初始化 cfd的监听属性。
tep.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
} else { cfd 们 满足读事件, 有客户端写数据来。
n = read(ep[i].data.fd, buf, sizeof(buf));
if ( n == 0) {
close(ep[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL); // 将关闭的cfd,从监听树上摘下。
} else if (n > 0) {
小--大
write(ep[i].data.fd, buf, n);
}
}
}
}
epoll 事件模型:
ET模式:
边沿触发:
缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
LT模式:
水平触发 -- 默认采用模式。
缓冲区剩余未读尽的数据会导致 epoll_wait 返回。
结论:
epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙轮询。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flg);
优缺点:
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
epoll 反应堆模型:
epoll ET模式 + 非阻塞、轮询 + void *ptr。
原来: socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- write回去。
反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件。
socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--
-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足
-- read() --- 小->大 -- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听写事件
-- 等待 epoll_wait 返回 -- 说明 cfd 可写 -- write回去 -- cfd从监听红黑树上摘下 -- EPOLLIN
-- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听读事件 -- epoll_wait 监听
eventset函数:
设置回调函数。 lfd --》 acceptconn()
cfd --> recvdata();
cfd --> senddata();
eventadd函数:
将一个fd, 添加到 监听红黑树。 设置监听 read事件,还是监听写事件。
tips
网络编程中: read --- recv()
write --- send();
read 函数的返回值:
>0: 实际读到的字节数
=0: socket中,表示对端关闭。close()
-1: 如果 errno == EINTR 被异常终端。 需要重启。
如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。 需要,再次读。
如果 errno == ECONNRESET 说明连接被 重置。 需要 close(),移除监听队列。
错误。
errno = “其他情况” 异常。
端口复用:
int opt = 1; // 设置端口复用。
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半关闭:
通信双方中,只有一端关闭通信。 --- FIN_WAIT_2
close(cfd);
shutdown(int fd, int how);
how: SHUT_RD 关读端
SHUT_WR 关写端
SHUT_RDWR 关读写
shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。
错误处理函数:
封装目的:
在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。
存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。
命名方式:系统调用函数首字符大写, 方便查看man手册
如:Listen()、Accept();
函数功能:调用系统调用函数,处理出错场景。
在 server.c 和 client.c 中调用 自定义函数
联合编译 server.c 和 wrap.c 生成 server
client.c 和 wrap.c 生成 client
readn:读 N 个字节
readline:读一行