网络编程入门02
在接触网络编程是,网上,基本的socket样例过于简单,公司的网络库匹配性太强,跟业务耦合比较大。所以阅读陈硕的muduo网络库.
namespace muduo::net::sockets
typedef struct sockaddr SA;
在socketAPI中:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
// 使用当前socket连接一个地址(与服务器建立正式连接),此函数会触发服务器端的accept、select函数
// 注意:服务端接收的socket值和客户端socket值不一样
// addr:一般是服务器地址
SocketsOps:
int connect(int sockfd, const struct sockaddr* addr) {
return ::connect(sockfd, addr, static_cast<socklen_t>(sizeof(struct sockaddr_in6)));
}
int bind(int socket,sockaddr * address,uint addrlen);
// 将一个地址和一个端口号绑定到一个socket连接上
// socket:之前创建的socket
// sockaddr:一个用来存放Ip地址和端口号的结构体
// addrlen:上述结构体的长度
// 返回值:为-1表示失败,若端口被占用,会从新绑定一个随机端口(仍返回失败)
// 地址绑定为0表示绑定本机所有IP
void sockets::bindOrDie(int sockfd, const struct sockaddr* addr)
{
int ret = ::bind(sockfd, addr, static_cast<socklen_t>(sizeof(struct sockaddr_in6)));
if (ret < 0)
{
LOG_SYSFATAL << "sockets::bindOrDie";
}
}
int listen(int socket,int maxconn);【仅TCP】【服务器】
// 将一个socket设置为监听状态,专门用来监听的socket叫做master socket
// maxconn:最大接收连接数
// 返回值:失败返回-1,成功返回0
void sockets::listenOrDie(int sockfd)
{
int ret = ::listen(sockfd, SOMAXCONN);
if (ret < 0)
{
LOG_SYSFATAL << "sockets::listenOrDie";
}
}
int accept(int socket,sockaddr * fromaddr,int * addrlen);【阻塞】【仅TCP】【服务器】
// 接收一个客户机的连接,返回一个socket,来自客户机的socket叫connected socket
// socket:用来监听的socket(master socket)
// fromaddr:客户机的地址信息
// addrlen:地址结构体的长度(输入输出参数)
// 返回值:返回一个新的socket,这个socket专门用来与此客户机通讯(connected socket)
int sockets::accept(int sockfd, struct sockaddr_in6* addr)
{
socklen_t addrlen = static_cast<socklen_t>(sizeof *addr);
#if VALGRIND || defined (NO_ACCEPT4)
int connfd = ::accept(sockfd, *sockaddr_cast*(addr), &addrlen);
setNonBlockAndCloseOnExec(connfd);
#else
int connfd = ::accept4(sockfd, sockaddr_cast(addr),
&addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
#endif
if (connfd < 0)
{
int savedErrno = errno;
LOG_SYSERR << "Socket::accept";
switch (savedErrno)
{
case EAGAIN:
case ECONNABORTED:
case EINTR:
case EPROTO: // ???
case EPERM:
case EMFILE: // per-process lmit of open file desctiptor ???
// expected errors
errno = savedErrno;
break;
case EBADF:
case EFAULT:
case EINVAL:
case ENFILE:
case ENOBUFS:
case ENOMEM:
case ENOTSOCK:
case EOPNOTSUPP:
// unexpected errors
LOG_FATAL << "unexpected error of ::accept " << savedErrno;
break;
default:
LOG_FATAL << "unknown error of ::accept " << savedErrno;
break;
}
}
return connfd;
}
*写了很多转换函数,转const sockaddr, sockaddr_in,sockaddr_in6等等*
const struct sockaddr* sockets::sockaddr_cast(const struct sockaddr_in6* addr)
{
return static_cast<const struct sockaddr*>(implicit_cast<const void*>(addr));
}
头文件:#include <unistd.h>
定义函数:ssize_t read(int fd, void * buf, size_t count);
函数说明:read()会把参数fd 所指的文件传送count 个字节到buf 指针所指的内存中. 若参数count 为0, 则read()不会有作用并返回0. 返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动.
附加说明:
如果顺利 read()会返回实际读到的字节数, 最好能将返回值与参数count 作比较, 若返回的字节数比要求读取的字节数少, 则有可能读到了文件尾。
ssize_t sockets::read(int sockfd, void *buf, size_t count)
{
return ::read(sockfd, buf, count);
}
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
两个函数的返回值:若成功则返回已读、写的字节数,若出错则返回-1
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。
readv则将读入的数据按上述同样顺序散布到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。
如果遇到文件结尾,已无数据可读,则返回0。
shutdown() 立即关闭socket; 并可以用来唤醒等待线程;
close() 不一定立即关闭socket(如果有人引用, 要等到引用解除);不会唤醒等待线程。
socketOps下的一些辅助函数:
void toIp(char* buf, size_t size, const struct sockaddr* addr);
把IP地址转化为字符串形式,存储在buf中
字节序是由cpu处理器架构决定的,和操作系统无关,例如Intel cpu采用小端字节序,PowerPC采用大端字节序,有些cpu例如Alpha支持两种字节序,但在使用时要设置具体采用哪一种字节序,不可能同时用两种。主机字节序(host byte order)就是指当前机器的字节序,根据cpu处理器的架构和设置,主机字节序可为小端字节序或大端字节序。
uint32_t networkToHost32(uint32_t net32){ //因为机器的大端小段问题,涉及到字节序问题。所以封装了一些列标准函数,
//完成本机字节序和wang网络字节序的相互转换
return be16toh(net16);
}
void sockets::toIpPort(char* buf, size_t size, const struct sockaddr* addr)
把socket地址 ip+port输出到buf中
void sockets::fromIpPort(const char* ip, uint16_t port, struct sockaddr_in* addr)
从ip,port 为 socket地址填入信息
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
参数:
sock:将要被设置或者获取选项的套接字。
level:选项所在的协议层。
optname:需要访问的选项名。
optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。
optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。
返回说明:
成功执行时,返回0。失败返回-1,errno被设为以下的某个值
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字
参数详细说明:
level指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项.
optname指定控制的方式(选项的名称),我们下面详细解释
optval获得或者是设置套接字选项.根据选项名称的数据类型进行转换
int getSocketError(int sockfd) {
getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen)
}
#include <sys/socket.h>
int getsockname(int sockfd,struct sockaddr* localaddr,socklen_t *addrlen);
int getpeername(int sockfd,struct sockaddr* peeraddr,socklen_t *addrlen);
struct sockaddr_in6 getLocalAddr(int sockfd);
struct sockaddr_in6 getPeerAddr(int sockfd); //把库函数进行了封装,使得用起来更加简单
bool isSelfConnect(int sockfd); 调用本地和对端,查看ip和端口
貌似比较重要的函数
void setNonBlockAndCloseOnExec(int sockfd)
{
// non-block
int flags = ::fcntl(sockfd, F_GETFL, 0);
flags |= O_NONBLOCK;
int ret = ::fcntl(sockfd, F_SETFL, flags);
// FIXME check
// close-on-exec
flags = ::fcntl(sockfd, F_GETFD, 0);
flags |= FD_CLOEXEC;
ret = ::fcntl(sockfd, F_SETFD, flags);
// FIXME check
(void)ret;
}
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
O_NONBLOCK和O_NDELAY所产生的结果都是使I/O变成非阻塞模式(non-blocking),在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。
它们的差别在于:在读操作时,如果读不到数据,O_NDELAY会使I/O函数马上返回0,但这又衍生出一个问题,因为读取到文件末尾(EOF)时返回的也是0,这样无法区分是哪种情况。因此,O_NONBLOCK就产生出来,它在读取不到数据时会回传-1,并且设置errno为EAGAIN
// 这里设置为FD_CLOEXEC表示当程序执行exec函数时本fd将被系统自动关闭,表示不传递给exec创建的新进程, 如果设置为fcntl(fd, F_SETFD, 0);那么本fd将保持打开状态复制到exec创建的新进程中 就是说如果有fork进程,某一个进程持有这个fd不会被克隆到子进程中
个人感受:
网络编程,linux原生的api非常的不好用,很多借口可能需要三四个参数,采用一定的封装性,很多函数只需要一个sockfd既可以调用,化繁为简。简单许多。
参考
socket关闭之close()和shutdown()的差异
网络转换函数
getsocketopt,setsocketopt函数说明
fnctl