作为一个宏大的、功能健全的muduo库,考虑的肯定是众多情况是否可以高效满足;而作为学习者,我们需要抽取其中的精华进行简要实现,这要求我们足够了解muduo库。
做项目 = 模仿 + 修改,不要担心自己学了也不会写怎么办,重要的是积累,学到了这些方法,如果下次在遇到通用需求的时候你能够回想起之前的解决方法就够了。送上一段话!
套接字(Socket)是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。它是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。简单来说,套接字是不同主机间的进程进行双间通信的端点,它构成了单个主机内及整个网络间的编程界面。
转自比特冬哥。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数类似于 open()函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符,通常把这个文件描述符称为 socket 描述符(socket descriptor),这个 socket 描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
该函数包括 3 个参数,如下所示:
domain
参数 domain 用于指定一个通信域;这将选择将用于通信的协议族。可选的协议族如下表所示:
对于 TCP/IP 协议来说,通常选择 AF_INET 就可以了,当然如果你的 IP 协议的版本支持 IPv6,那么可以选择 AF_INET6。
type
参数 type 指定套接字的类型,当前支持的类型有:
protocol
参数 protocol 通常设置为 0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为SOCK_STREAM 的默认协议是传输控制协议(Transmission Control Protocol,TCP 协议)。
在 AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP。
调用 socket()与调用 open()函数很类似,调用成功情况下,均会返回用于文件 I/O 的文件描述符,只不过对于 socket()来说,其返回的文件描述符一般称为 socket 描述符。当不再需要该文件描述符时,可调用close()函数来关闭套接字,释放相应的资源。
如果 socket()函数调用失败,则会返回-1,并且会设置 errno 变量以指示错误类型。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数用于将一个 IP 地址或端口号与一个套接字进行绑定(将套接字与地址进行关联)。将一个客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。一般来讲,会将一个服务器的套接字绑定到一个众所周知的地址—即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址(注意这里说的地址包括 IP 地址和端口号)。因为对于客户端来说,它与服务器进行通信,首先需要知道服务器的 IP 地址以及对应的端口号,所以通常服务器的 IP 地址以及端口号都是众所周知的。
调用 bind()函数将参数 sockfd 指定的套接字与一个地址 addr 进行绑定,成功返回 0,失败情况下返回-1,并设置 errno 以提示错误原因。
关于sockaddr的讲解,请看我InetAddress那篇文章。
listen()函数只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函数在一般在 bind()函数之后调用,在 accept()函数之前调用,它的函数原型是:
int listen(int sockfd, int backlog);
无法在一个已经连接的套接字(即已经成功执行 connect()的套接字或由 accept()调用返回的套接字)上执行 listen()。
参数 backlog 用来描述 sockfd 的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为 TCP 连接是一个过程,由于同时尝试连接的用户过多,使得服务器进程无法快速地完成所有的连接请求,那怎么办呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限,这个 backlog 参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。
服务器调用 listen()函数之后,就会进入到监听状态,等待客户端的连接请求,使用 accept()函数获取客户端的连接请求并建立连接。函数原型如下所示:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
① 调用 socket()函数打开套接字;
② 调用 bind()函数将套接字与一个端口号以及 IP 地址进行绑定;
③ 调用 listen()函数让服务器进程进入监听状态,监听客户端的连接请求;
④ 调用 accept()函数处理到来的连接请求。
accept()函数通常只用于服务器应用程序中,如果调用 accept()函数时,并没有客户端请求连接(等待连接队列中也没有等待连接的请求),此时 accept()会进入阻塞状态,直到有客户端连接请求到达为止。当有客户端连接请求到达时,accept()函数与远程客户端之间建立连接,accept()函数返回一个新的套接字。这个套接字与 socket()函数返回的套接字并不同,socket()函数返回的是服务器的套接字(以服务器为例),而accept()函数返回的套接字连接到调用 connect()的客户端,服务器通过该套接字与客户端进行数据交互,譬如向客户端发送数据、或从客户端接收数据。
所以,理解 accept()函数的关键点在于它会创建一个新的套接字,其实这个新的套接字就是与执行
connect()(客户端调用 connect()向服务器发起连接请求)的客户端之间建立了连接,这个套接字代表了服务器与客户端的一个连接。如果 accept()函数执行出错,将会返回-1,并会设置 errno 以指示错误原因。
参数 addr 是一个传出参数,参数 addr 用来返回已连接的客户端的 IP 地址与端口号等这些信息。
参数addrlen 应设置为 addr 所指向的对象的字节长度,如果我们对客户端的 IP 地址与端口号这些信息不感兴趣,可以把 arrd 和 addrlen 均置为空指针 NULL。
这个函数相比于accept多了一些接受的选项,如SOCK_NONBLOCK和SOCK_CLOEXEC.
SOCK_NONBLOCK是一个套接字选项,用于设置或查询套接字的阻塞模式。在阻塞模式下,套接字会等待操作完成或出现错误才会返回,而在非阻塞模式下,套接字会立即返回,不会等待操作完成或出现错误。等价于O_NONBLOCK。
SOCK_CLOEXEC是一个套接字选项,用于设置或查询套接字的close-on-exec标志位。当进程执行新程序时,如果套接字的close-on-exec标志位被设置,则该套接字将被自动关闭。设置close-on-exec标志位的好处是,当进程执行新程序时,可以避免套接字被继承到新程序中,从而避免了潜在的安全问题。
connect()函数原型如下所示:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
该函数用于客户端应用程序中,客户端调用 connect()函数将套接字 sockfd 与远程服务器进行连接,参数 addr 指定了待连接的服务器的 IP 地址以及端口号等信息,参数 addrlen 指定了 addr 指向的 struct sockaddr对象的字节大小。
客户端通过 connect()函数请求与服务器建立连接,对于 TCP 连接来说,调用该函数将发生 TCP 连接的握手过程,并最终建立一个 TCP 连接,而对于 UDP 协议来说,调用这个函数只是在 sockfd 中记录服务器IP 地址与端口号,而不发送任何数据。
函数调用成功则返回 0,失败返回-1,并设置 errno 以指示错误原因。
套接字成员。
TCP_NODELAY
setsockopt(sockfd_, IPPROTO_IP, TCP_NODELAY, &flag, sizeof flag);
这段代码是C或C++语言中用于设置套接字选项的函数调用。具体来说,它设置了一个TCP套接字的Nagle算法的禁用选项。
综合来看,这段代码的作用是禁用套接字sockfd_上的Nagle算法,以加快数据传输速度。这在实时应用或需要低延迟的应用中可能是有用的。
SO_REUSEADDR
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof flag);
这段代码是C或C++语言中用于设置套接字选项的函数调用。具体来说,它设置了一个套接字的地址复用选项。
setsockopt() 是一个用于设置套接字选项的函数。
sockfd_ 是要设置选项的套接字的文件描述符。
SOL_SOCKET 是协议级别,表示我们正在设置套接字级的选项。
SO_REUSEADDR 是一个选项,用于允许套接字在关闭后立即重新使用其地址。这在进行服务器编程时特别有用,因为在服务器重启或关闭后,它通常需要立即重新绑定到相同的地址和端口。
&flag 是指向一个整数的指针,该整数表示是否启用地址复用(flag 为非零值表示启用,为零表示禁用)。
sizeof flag 是选项的长度。
综合来看,这段代码的作用是启用套接字sockfd_上的地址复用功能,以便在关闭后立即重新使用相同的地址。这在服务器编程中是常见的做法,以避免由于地址绑定延迟而导致的延迟或问题。
SO_REUSEPORT
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &flag, sizeof flag);
这段代码是C或C++语言中用于设置套接字选项的函数调用。具体来说,它设置了一个套接字的端口复用选项。
setsockopt() 是一个用于设置套接字选项的函数。
sockfd_ 是要设置选项的套接字的文件描述符。
SOL_SOCKET 是协议级别,表示我们正在设置套接字级的选项。
SO_REUSEPORT 是一个选项,用于允许多个套接字绑定到同一个端口上。这在进行服务器编程时特别有用,特别是在使用如Nginx这样的高性能服务器时,它可以允许多个工作进程绑定到同一个端口上,从而实现负载均衡和容错。
&flag 是指向一个整数的指针,该整数表示是否启用端口复用(flag 为非零值表示启用,为零表示禁用)。
sizeof flag 是选项的长度。
综合来看,这段代码的作用是启用套接字sockfd_上的端口复用功能,以便允许多个套接字绑定到同一个端口上。这在实现高性能服务器和负载均衡时是常见的做法。
SO_KEEPALIVE
setsockopt(sockfd_, SOL_SOCKET, SO_KEEPALIVE, &flag, sizeof flag);
这行代码具体设置了一个套接字的 “Keep-Alive” 选项。让我们逐一解析这行代码的各个部分:
sockfd_:
这是要设置选项的套接字的文件描述符。
SOL_SOCKET:
这是协议级别,表示我们正在设置的是套接字级的选项,而不是某个特定于协议的选项。
SO_KEEPALIVE:
这是一个套接字选项,用于启用或禁用 “Keep-Alive” 功能。启用这个功能后,如果套接字在一段时间内没有活动,操作系统会发送一个数据包来检查连接是否仍然有效。这对于检测网络故障或对端系统故障非常有用。
&flag:
这是一个指向整数的指针,表示是否启用 “Keep-Alive” 功能。如果 flag 为非零值,则启用该功能;如果为零,则禁用。
sizeof flag:
这表示选项的长度,这里是 flag 变量的大小。
总结:这行代码用于设置 sockfd_ 套接字的 “Keep-Alive” 功能。如果 flag 非零,则启用该功能;如果为零,则禁用。启用 “Keep-Alive” 可以帮助检测和预防网络连接问题。
//Socket.h
#pragma once
#include <sys/socket.h>
#include <netinet/tcp.h>
#include "Log.h"
#include "noncopyable.h"
#include "InetAddress.h"
class Socket : noncopyable {
public:
explicit Socket(int sockfd) : sockfd_(sockfd) {}
~Socket();
int fd() const { return sockfd_; }
void listen();
void bind(const InetAddress& localAddr);
int accept(InetAddress& peerAddr);
void setTcpNoDelay(bool on);
void setReuseAddr(bool on);
void setReusePort(bool on);
void setKeepAlive(bool on);
private:
const int sockfd_;
};
//Socket.cc
#include "Socket.h"
Socket::~Socket() {
::close(sockfd_);
}
void Socket::listen() {
if(::listen(sockfd_, 1024) == -1) {
LOG_FATAL("%s--%s--%d--%d : listen error\n", __FILE__, __FUNCTION__, __LINE__, errno);
}
}
void Socket::bind(const InetAddress& localAddr) {
if (::bind(sockfd_, (sockaddr*)localAddr.getSockAddr(), sizeof(sockaddr)) != 0) {
LOG_FATAL("%s--%s--%d--%d : bind error\n", __FILE__, __FUNCTION__, __LINE__, errno);
}
}
int Socket::accept(InetAddress& peerAddr) {
sockaddr_in* sa;
bzero(sa, sizeof sa);
socklen_t st = sizeof(sa);
int connfd = ::accept4(sockfd_, (sockaddr*)sa, &st, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (connfd >= 0) {
peerAddr.setSockAddr(*sa);
}
return connfd;
}
void Socket::setTcpNoDelay(bool on) {
int flag = on ? 1 : 0;
::setsockopt(sockfd_, IPPROTO_IP, TCP_NODELAY, &flag, sizeof flag);
}
void Socket::setReuseAddr(bool on) {
int flag = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof flag);
}
void Socket::setReusePort(bool on) {
int flag = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &flag, sizeof flag);
}
void Socket::setKeepAlive(bool on) {
int flag = on ? 1 : 0;
::setsockopt(sockfd_, SOL_SOCKET, SO_KEEPALIVE, &flag, sizeof flag);
}
以上就是套接字Socket类的相关介绍,以及我在进行项目重写的时候遇到的一些问题,和我自己的一些心得体会。发现写博客真的会记录好多你的成长,而且对于一个好的项目,写博客也是证明你确实有过深度思考,并且在之后面试或者工作时遇到同样的问题能够进行复盘的一种有效的手段。所以,希望uu们也可以像我一样,养成写博客的习惯,逐渐脱离菜鸡队列,向大佬前进!!!加油!!!
也希望我能够完成muduo网络库项目的深度学习与重写,并在功能上能够拓展。也希望在完成这个博客系列之后,能够引导想要学习muduo网络库源码的人,更好地探索这篇美丽繁华的土壤。致敬chenshuo大神!!!
鉴于博主只是一名平平无奇的大三学生,没什么项目经验,所以可能很多东西有所疏漏,如果有大神发现了,还劳烦您在评论区留言,我会努力尝试解决问题!