实现一个 telnet 版本的聊天服务器,主要有以下需求。
telnet ip:port
的方式连接到服务器上。实现一个简单的 HTTP 服务器,主要有以下需求。
实现一个生产者 / 消费者消息队列服务,主要有以下需求。
v2ray 文档 https://www.v2ray.com/developer/intro/roadmap.html
自己实现一个 socks5
Python: https://hatboy.github.io/2018/04/28/Python%E7%BC%96%E5%86%99socks5%E6%9C%8D%E5%8A%A1%E5%99%A8
早起 TCP/IP 被移植到 UNIX 平台时,设计者们希望像访问文件一样去访问网络。
Linux 提供了三种类型套接口:
提供了可靠的双向顺序数据流连接。
提供双向的数据传输。
这种套接口允许进程直接存取下层的协议。
现在全世界的人都在解决 C10K 问题。
http://www.kegel.com/c10k.html
翻译版:https://www.oschina.net/translate/c10k
read
、write
。
而且这里的 read 将不会受到来自其他主机的应答。
send(sockfd, 'A', 1, MSG_OOB)
两个缓冲区:内核缓冲区、进程缓冲区,当内核缓冲区未满足时,该进程将被投入休眠。
将一个套接口设为非阻塞 => 通知内核,当所请求的 I/O 操作未满足时,不要阻塞该进程,而是返回一个错误
优点:当 I/O 操作不能立即完成时,进程还可以继续后续的操作,提高自身运行效率。
缺点:进程一直处于运行状态,可能占用大量CPU时间,影响其他进程的运行效率。
非阻塞connect三个用途
1.设置套接口为非阻塞
2.发起非阻塞 connect
3.等待连接建立期间完成其他事情
4.检查连接是否立即建立
5.调用 select
6.处理 select 超时
7.检查可读可写条件,调用 getsockopt 查看连接是否成功
8.关闭非阻塞状态并返回
可等待多个描述字的就绪
内核在描述字就绪时,发送 SIGIO 信号通知进程
绑定信号以及对应的处理函数 => 继续执行其他操作 => 满足后自动处理
告知内核启动某个操作,并让内核在整个操作完成(包括将数据从内核拷贝到进程缓冲区里)后通知 与信号驱动的区别:
aio_read
给内核传递描述字、缓冲区指针、缓冲区大小、文件偏移,并告诉内核当操作完成时如何通知进程。
哪种情况下适合采用阻塞式I/O编程?
访问一个或多个服务进程时,各访问之间有顺序关系
非阻塞与阻塞在 CPU 利用率上有什么区别
阻塞期间不占用 CPU 时间,不影响其他进程的工作效率,进程可能长时间处于休眠,在此期间进程不能执行别的任务,进程自身的效率不高。
非阻塞,进程还可以执行后续的任务,提高自身的工作效率,进程一直处于执行期间,可能占用大量CPU时间来检测IO操作是否完成,影响其他进程的执行效率。
哪些套接口会发生阻塞
// 数据发送 发送缓冲区没有空间
sendmsg, sendto, send, write, writev
// 数据接收,接收缓冲区没有空间
recvmsg, recvfrom, recv, read, readv
// 完成三次握手
connect
// 无新连接到达
accept
局限于局域网内使用
局域网、跨广域网都可使用
常见信号及其默认动作
SIGABRT 异常终止(abort) 终止
SIGFPE 算术异常(除以0) 终止
SIGUSR1 用户定义信号 忽略
SIGUSR2 同上
SIGHUP 连接断开(送给控制进程) 终止
SIGALRM 计时器到时(alarm) 终止
SIGCHLD 子进程状态改变 忽略
SIGURG 紧急数据到达 忽略
SIGIO 异步I/O 终止
SIGINT 终端中断符 终止
SIGPIPE 写至无读进程的管套 终止
SIGKILL 终止进程 终止
消息通信通过消息队列实现进程通信
进程能够不涉及内核而访问其中的数据
异步I/O模型的发展技术是: select -> poll -> epoll -> aio -> libevent -> libuv。Unix/Linux用了好几十年走过这些技术的变迁,然而,都不如 Windows I/O Completion Port 设计得好(免责声明:这个观点纯属个人观点。相信你仔细研究这些I/O模型后,你会得到你自己的判断)。
由于 socket 是文件描述符,因而某个线程盯的所有的 socket,都放在一个文件描述符集合 fd_set 中,这就是项目进度强,然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 中对应的位都设为 1,表示 socket 可读或者可写,从而可以进行读写操作,然后再调用 select,接着盯下一轮的变化。
上面的方式在文件描述符有变化时,都会采用轮询的方式确定具体是哪个 socket 有变化,也就是需要将全部项目都过一遍的方式来查看进度,这就大大影响了一个项目组能够支撑的最大的项目数量。因而使用 select,能够同时盯的项目数量由 FD_SETSIZE 限制。
如果改成事件通知的方式,情况就会好很多。(select 里不能返回具体是哪个 socket 变化了?)
最终方式:epoll + callback
AIO: Asynchronous IO ,异步非阻塞
BIO:Block-io,同步且阻塞式IO
NIO:Non-block IO,同步非阻塞
int to char array
sprintf(buf, "%d", num);
端口复用(注意放到 bind 前面)
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#include <linux/sock.h>
// 通用型套接字地址结构
struct sockaddr {
unsigned short sa_family; // 地址类型,AF_xxx,2 个字节
char sa_data[14]; // 协议地址,14 个字节
}
// ipv4
struct sockaddr_in {
short int sin_family; // 地址类型:AF_INET
unsigned short int sin_port;// 端口号,16 位 TCP/UDP 端口号网络字节顺序
struct in_addr sin_addr; // 32 位地址
unsigned char sin_zero[8];
}
字节操纵
#include <string.h>
void bzero(void* s, size_t n); // n 个字节置零
void bcopy(const void* src, void* dest, size_n); // 拷贝 n 个字节
int bcmp(const void* s1, const void* s2, size_t n); // 相等返回 0
#include <string.h>
void *memset(void *s, int c, size_t n); // 将目标中n个字节设置为值c
void *memcpy(void *dest, const void *src, size_t n); // 拷贝字符串中n个字节
int memcmp(const void *s1, const void *s2, size_t n); // 字符串比较,相等返回0;不等返回非0
IP 地址转换
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* h 表示 host,n 表示 network,l 表示 32 位整数,s 表示 16 位短整数 */
// 点分十进制字符串 => 网络字节顺序二进制值
int inet_aton(const char *cp, struct in_addr *inp);
// 点分十进制字符串 => 网络字节顺序二进制值
unsigned long int inet_addr(const char *cp);
// 以255.255.255.255表示出错,不能表示此广播地址
// 网络字节顺序二进制值 => 点分十进制字符串
char * inet_ntoa(struct in_addr in);
int inet_pton(int af, const char * strptr, void *dst);
// 成功返回1;输入无效表达式格式返回0;出错返回-1
const char* inet_ntop(int af, const void * strptr, char *dst, size_t cnt);
// 成功返回结果指针dst;出错返回NULL
> #include<sys/types.h> > #include<sys/socket.h> >
socket
int socket(int domain, int type, int protocol);
// domain:协议族。type:套接口类型,protocol:协议类型
// 返回值:-1 出错,非负值则为套接口描述字
int socketpair(int family, int type, int protocol, int fd_array[2]);
bind
将套接口指定IP、port,可两者都指定,也可都不指定; 服务端通常在启动时绑上端口; 客户端通常不绑定端口,由内核分配临时端口; 可通过
getsockname
来返回协议地址。
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
// 成功则返回0,失败返回-1
listen
监听本地地址和端口 套接口状态:closed => listen sockfd-已绑定的socket描述符 backlog-已完成连接、等待接收的队列长度,LISTENQ?
int listen(int sockfd, int backlog);
// 成功则返回0,失败返回-1,错误原因存于errno
accept
当服务请求到达 accept 监视的 socket(监听套接口)时,系统将自动建立一个新的 socket(已连接套接口),并将此 socket 和客户进程连接起来。
int accept(int sockfd, sockaddr* cliaddr, int *addrlen);
#include <unistd.h>
read
从套接口接收缓冲区中读取len字节的数据,成功返回,返回值是实际读取数据的字节数
ssize_t read(int fd, void *buf, size_t count);
/* 返回值:
无数据 => 阻塞
n >= len => len
n > 0 && n < len => 读取 n 个
n = 0 => 读通道关闭
n < 0 => 出错或异常
n = -1, errno == EINTR => 读中断引起错误
n = -1, errno == ECONNREST => 网络连接有问题
read 函数要求操作系统内核从套接字描述字 socketfd读取最多多少个字节(size),并将结果存储到 buffer 中。返回值告诉我们实际读取的字节数目,也有一些特殊情况,如果返回值为 0,表示 EOF(end-of-file),这在网络中表示对端发送了 FIN 包,要处理断连的情况;如果返回值为 -1,表示出错。
*/
/* 从 socketfd 描述字中读取 "size" 个字节. */
ssize_t readn(int fd, void *vptr, size_t size) {
size_t nleft = size;
ssize_t nread;
char* ptr = vptr;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0; /* 这里需要再次调用 read */
else
return(-1);
} else if (nread == 0)
break; /* EOF(End of File) 表示套接字关闭 */
nleft -= nread;
ptr += nread;
}
return n - nleft; /* 返回的是实际读取的字节数 */
}
write
从套接口中发送 len 字节的数据,成功返回,返回实际写入数据的字节数
ssize_t write(int fd, const void *buf, size_t count);
recv
int recv(int fd, void *buf, int len, unsigned int flags);
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
send
int send(int fd, const void *msg, int len, unsigned int flags);
sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// socklen_t 不需要指针
recvmsg
sendmsg
connect
TCP 客户端与服务器建立连接用 connect 函数
int connect(int sockfd, struct sockaddr * addressp, int addrlen);
shutdown
终止网络连接并停止所有信息的发送与接收(不管引用计数器为何值)
#include <sys/socket.h>
int shutdown(int sockfd, int how);
// sockfd:套接口描述字
// how:套接口关闭方式,SHUT_RD、SHUT_WR、SHUT_RDWR
close
计数器减一,不会完全关闭
getsockopt/setsockopt
gethostbyaddr getaddrbyhost,...
int flag = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flag|O_NONBLOCK);
int nIO = 1;
ioctl(sockfd, FIONBIO, &nIO);
fd_set
void FD_ZERO(fd_set * fdset); // 清除描述字集 fdset 中的所有位
void FD_SET(int fd, fd_set *fdset); // 在 fdset 集中加入fd描述字(为什么要事先添加?
void FD_CLR(int fd, fd_set *fdset); // 将 fd 从 fdset 中清除
int FD_ISSET(int fd, fd_set *fdset); // 判断 fd 是否在 fdset 中(而不是看是否为1?
select
#include <sys/select.h>
#include <sys/time.h>
int select(int fdmax, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);
// select 后,要注意复原 fd_set
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// POLLIN / POLLOUT / POLLERR
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 需要等待的事件 */
short revents; /* 实际发生了的事件,返回值 */
};
epoll
//创建 epoll
int epfd = epoll_crete(1000);
//将 listen_fd 添加进 epoll 中
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd,&listen_event);
while (1) {
//阻塞等待 epoll 中 的fd 触发
int active_cnt = epoll_wait(epfd, events, 1000, -1);
for (i = 0 ; i < active_cnt; i++) {
if (evnets[i].data.fd == listen_fd) {
//accept. 并且将新accept 的fd 加进epoll中.
}
else if (events[i].events & EPOLLIN) {
//对此fd 进行读操作
}
else if (events[i].events & EPOLLOUT) {
//对此fd 进行写操作
}
}
}
static void sig_alrm(int signo) {
return; // 这里的处理对原阻塞是怎么处理的?
}
signal(SIGALRM, sig_alrm); // 绑定信号处理函数
alarm(3);