socket 开发 - 那些年用过的基础 API

--------------------------------------------------------------------------------------------------------------------------------------------------

前言 - 思考还是

--------------------------------------------------------------------------------------------------------------------------------------------------

  socket 写过一点点,  总感觉很别扭. 例如 read, recv, recvfrom 这些为啥这么奇葩. 这是 linux 的设计吗.

这种强糅合的 read 代码, '带坏'了多少人. 想起很久以前看过的 <<UNIX痛恨者手册>>, 外加上常写点跨平台

库. 不得不思考设计, 发现 

  1) winds 对于 socket 设计比 linux POSIX 设计理解更加友好一丢丢

  2) linux 性能比 winds 好. (开源哲学 对冲 精英文化)

  3) 应用层是个不完备的域, 不要一条胡同走不到头

(备注 : 有一段日子特别讨厌 winds, 及其喜欢羡慕 unix, 但是随着成长认识有了很大变化, 痛恨没钱没时间)

--------------------------------------------------------------------------------------------------------------------------------------------------

正文 - 来点证明

--------------------------------------------------------------------------------------------------------------------------------------------------

1. 如果可以不妨多写点跨平台, 线程安全的代码

  不妨举个烂大街的例子, 我们经常在处理时间的时候直接用  gettimeofday

#include <sys/time.h>

int gettimeofday(struct timeval * tv, struct timezone * tz);

The  functions  gettimeofday() can get and set the time as well as a timezone. 
The tv argument is a struct timeval (as specified in <sys/time.h>):

    struct timeval {
        time_t      tv_sec;     /* seconds */
        suseconds_t tv_usec;    /* microseconds */
    };

and gives the number of seconds and microseconds since the Epoch (see time(2)).  
The tz argument is a struct timezone:

    struct timezone {
        int tz_minuteswest;     /* minutes west of Greenwich */
        int tz_dsttime;         /* type of DST correction */
    };

If either tv or tz is NULL, the corresponding structure is not set or returned.  
(However, compilation warnings will result if tv is NULL.)

The use of the timezone structure is obsolete; 
the tz argument should normally be specified  as  NULL.

只是简单的得到当前时间秒数和微秒, 附赠一个时区消息. 这个函数一眼看过去, 设计的不优美.

如果希望你的代码能够在 winds 上面也奔跑, 可能需要一个移植版本 

#ifdef _MSC_VER

#include <winsock2.h>
// // gettimeofday - Linux sys/time.h 中得到微秒的一种实现 // tv : 返回结果包含秒数和微秒数 // tz : 包含的时区,在winds上这个变量没有用不返回 // return : 默认返回0 // inline int gettimeofday(struct timeval * tv, void * tz) { struct tm st; SYSTEMTIME wtm; GetLocalTime(&wtm); st.tm_year = wtm.wYear - 1900; st.tm_mon = wtm.wMonth - 1; // winds的计数更好些 st.tm_mday = wtm.wDay; st.tm_hour = wtm.wHour; st.tm_min = wtm.wMinute; st.tm_sec = wtm.wSecond; st.tm_isdst = -1; // 不考虑夏令时 tv->tv_sec = (long)mktime(&st); // 32位使用数据强转 tv->tv_usec = wtm.wMilliseconds * 1000; // 毫秒转成微秒 return 0; } #endif

同样你的工作量已经起来了. 不管高不高效. 总是个下策. 这里有个更好的主意, 利用  timespec_get 

#include <time.h>


/* Set TS to calendar time based in time base BASE.  */
int
timespec_get (struct timespec *ts, int base)
{
  switch (base)
    {
    case TIME_UTC:
      if (__clock_gettime (CLOCK_REALTIME, ts) < 0)
        return 0;
      break;

    default:
      return 0;
    }

  return base;
}

C11 标准提供的获取秒和纳秒的时间函数, CL 和 GCC clang 都提供了支持. 上面是glibc中一个实现, 是不是很 low.

扯一点

  1.1 写代码应该有很强的目的, 非特殊领域应该弱化针对性

  1.2 上层应用, 应该首要向着标准靠拢, 其次是操作系统, 再到编译器

对于CL 实现了 timespec_get, 应该最主要目的是为了 C++11基础特性支持, 还有 clang 的实现.

--------------------------------------------------------------------------------------------------------------------------------------------------

2. 你是否和我一样曾经因为 WSAStartup 大骂微软SB

  写 socket winds 一定会有下面三部曲, 或者两部曲. 

// 1. CL 编译器 设置
引入库 ws2_32.lib 
引入宏 _WINSOCK_DEPRECATED_NO_WARNINGS

// 2. 加载 socket dll
    WSADATA wsad;
    WSAStartup(WINSOCK_VERSION, &wsad);

// 3. 卸载 
    WSACleanup

当时想, linux 为啥木有上面这么无意义的操作.  其实其中有个故事, 当初微软不得了时期, 无法和unix socket互连.

后面来回扯, 其它无数巨擎给其 Winsock 升级, dll 版本变化厉害. 所以有了上面抛给用户层加载绑定dll版本的操作.

那么再linux 上面真的不需要吗. 其实也需要, 只是在运行 _start 时候帮助我们做了. 所以这点上面完全可以这么

封装 

//
// socket_init - 单例启动socket库的初始化方法
//  
inline void socket_init(void) {
#ifdef _MSC_VER
    WSADATA wsad;
    WSAStartup(WINSOCK_VERSION, &wsad);
#elif __GUNC__
    signal(SIGPIPE, SIG_IGN)  
#endif
}

--------------------------------------------------------------------------------------------------------------------------------------------------

3. 还记得 read, recv, recvfrom 吗 ?

  还处在一切皆文件支配的恐惧中吗. 实现这种思路无外乎注册和switch工厂分支. 那就意味着 read 是个杂糅

体. 在我们只是使用 socket fd 读取的时候 最终 read -> recv 这个函数调用, 即 recv(fd, buf, sz, 0). 对于后者 

ssize_t
__libc_recv (int fd, void *buf, size_t len, int flags)
{
#ifdef __ASSUME_RECV_SYSCALL
  return SYSCALL_CANCEL (recv, fd, buf, len, flags);
#elif defined __ASSUME_RECVFROM_SYSCALL
  return SYSCALL_CANCEL (recvfrom, fd, buf, len, flags, NULL, NULL);
#else
  return SOCKETCALL_CANCEL (recv, fd, buf, len, flags);
#endif
}

可以表明 recv 和  recvfrom 实现层面有过纠缠. 但是和 read 上层没有耦合. 所以对于单纯 TCP socket 最好的

做法还是 recv 走起. 

       #include <sys/types.h>
       #include <sys/socket.h>

       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);

其中对于 recv flags 有下面几个多平台都支持的宏 

#define MSG_OOB         0x1             /* process out-of-band data */
#define MSG_PEEK        0x2             /* peek at incoming message */
#define MSG_DONTROUTE   0x4             /* send without using routing tables */

#if(_WIN32_WINNT >= 0x0502)
#define MSG_WAITALL     0x8             /* do not complete until packet is completely filled */
#endif //(_WIN32_WINNT >= 0x0502)

其实开发中, MSG_OOB 带外数据, 除非学习. 否则无意义. MSG_PEEK 在以前的 \r\n 切分流协议的时候还用.

现在基本都没有场景. MSG_WAITALL 可以尝试一下替代很久以前的 for read. 可以有轻微提升性能. 

recv(fd, buf, len, 0) or recv(fd, buf, len, MSG_WAITALL) 用在你的常说的'高性能'服务器中而不是大杂烩 read.

--------------------------------------------------------------------------------------------------------------------------------------------------

4. 是否为 listen, accept 好奇过 !

  首先从 listen 和 accept 一对好cp说起. 其实大体过程无外乎 listen -> connect -> accept .  这里只是从用法

而言首先看 listen 部分 

/*
 *    Perform a listen. Basically, we allow the protocol to do anything
 *    necessary for a listen, and if that works, we mark the socket as
 *    ready for listening.
 */

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned int)backlog > somaxconn)
            backlog = somaxconn;

        err = security_socket_listen(sock, backlog);
        if (!err)
            err = sock->ops->listen(sock, backlog);

        fput_light(sock->file, fput_needed);
    }
    return err;
}

这段 listen 代码写得真好看. 我从中看出来, 内核的思路还是注册.  对于 backlog 存在一个最大值.

所以对于高性能服务器 listen 正确的写法推荐 

listen(fd, SOMAXCONN)

把 listen创建的监听和链接成功队列大小交给操作系统的内核配置. 

对于 accept 原本想讲一讲 accept4 + SOCK_NONBLOCK 降低 socket 开发流程. 但是一想起 unix or winds

应该不支持算了. 还是老实 accept + O_NONBLOCK. 

SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen)
{
    return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}

突然意识到优化就是生命枯竭, 打击痛点才是王道.

--------------------------------------------------------------------------------------------------------------------------------------------------

5. 你为 select 苦恼过吗, 去它的 poll 

  其实想想 select 这种函数设计的真的很奇葩. select -> poll -> epoll 从床上到床下经历过多少夜晚. 

主要是 winds 和 linux 对于 select 完全是两个函数, 恰巧名字一样. 通过下面一个不好的材料了解

一个真正的客户端非阻塞的 connect

select 开发中的用法. 为什么讲 select, 因为方便 winds 移植调试 !! iocp很吊但是真的很难把它和epoll

揉在一起. 因为二者都很意外. epoll 是 61 + 10 分 一个iocp是 90 - 20 分. 如果强揉就要对 socket 行为

读写链接都需要抽出一层. 但是用 select 只需要抽出 poll 监听触发抽出来就可以了. 后期有时间我们

详细分析 iocp. 当前带大家感受下 epoll 那些操作.

#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flags);

epoll_create()  creates a new epoll(7) instance.  Since Linux 2.6.8, the size argument is 
ignored, but must be greater than zero; see NOTES below. epoll_create() returns a file descriptor referring to the
new epoll instance. This file
descriptor is used for all the subse‐quent calls to the epoll interface. When no longer
required, the file descriptor returned by epoll_create() should be closed by
using close(2).
When all file descriptors referring to an epoll instance have been closed, the kernel
destroys the instance and releases the associated resources
for reuse. epoll_create1() If flags is 0, then, other than the fact that the obsolete size argument is dropped,
epoll_create1() is the same as epoll_create(). The following value can be included in
flags to obtain different behavior: EPOLL_CLOEXEC Set the close
-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of
the O_CLOEXEC flag in open(2) for reasons why this may be useful.

更加具体是

SYSCALL_DEFINE1(epoll_create, int, size)
{
    if (size <= 0)
        return -EINVAL;

    return sys_epoll_create1(0);
}

从上面可以看出来目前推荐的 epoll_create 用法是 

epoll_create1(EPOLL_CLOEXEC)

不再需要 size这个历史包袱, 并且 exec 重新开进程的时候能够 close 返回的 efd 防止句柄泄漏. 

还有一个就是关于 epoll 的 EPOLLIN 默认LT水平触发状态, 另外一个是 EPOLLET 边缘触发. 

/* Flags for epoll_create1.  */

#define EPOLL_CLOEXEC O_CLOEXEC


/* Valid opcodes to issue to sys_epoll_ctl() */

#define EPOLL_CTL_ADD 1
#define EPOLL_CTL_DEL 2
#define EPOLL_CTL_MOD 3


/* Epoll event masks */

#define EPOLLIN     0x00000001
#define EPOLLPRI    0x00000002
#define EPOLLOUT    0x00000004
#define EPOLLERR    0x00000008
#define EPOLLHUP    0x00000010

/* Set the Edge Triggered behaviour for the target file descriptor */

#define EPOLLET (1U << 31)

对于普通服务器例如游戏服务器, 大型Web系统服务器 LT 这种高级 select 操作就足够了.  刚好把验证

代码抛给上层. ET 模式的话就需要在框架的网络层处理包异常. 但是安全的高速度的通道通信可以尝试

一套ET流程交互. epoll 功能特别好理解, 注册, 监听, 返回结果. 最恶心就是返回结果的操作. 

不妨展示个局部代码 

//
// sp_wait - poll 的 wait函数, 等待别人自投罗网
// sp       : poll 模型
// e        : 返回的操作事件集
// max      : e 的最大长度
// return   : 返回待操作事件长度, <= 0 表示失败
//
int 
sp_wait(poll_t sp, struct event e[], int max) {
    struct epoll_event ev[max];
    int i, n = epoll_wait(sp, ev, max, -1);

    for (i = 0; i < n; ++i) {
        uint32_t flag = ev[i].events;
        e[i].s = ev[i].data.ptr;
        e[i].write = flag & EPOLLOUT;
        e[i].read = flag & (EPOLLIN | EPOLLHUP);
        e[i].error = flag & EPOLLERR;
    }

    return n;
}

一个最简单的展示结果, 这里就处理了 EPOLLOUT 和 EPOLLHUP 还有 EPOLLERR 枚举.

EPOLLHUP 解决 listen -> connect -> accept 占用资源不释放, 空转问题. 其实想想最简单的TCP网络也不好搞.

要求很多 (网络细节, 是个大工程)

--------------------------------------------------------------------------------------------------------------------------------------------------

6. 讲的有点泛泛, 文末不妨展示个 不忘初心 

#include <stdio.h>
#include <limits.h>
#include <stdint.h>

//
// 强迫症 × 根治
// file : len.c
// make : gcc -g -Wall -O2 -o love.out love.c
// test : objdump -S love.out
//
int main(int argc, char * argv[]) {
    const char heoo[] = "Hello World";

    for (size_t i = sizeof heoo - 1; i < SIZE_MAX; --i)
        printf(" %c", heoo[i]);
    putchar('\n');

    return 0;
}

--------------------------------------------------------------------------------------------------------------------------------------------------

后记 - 力求走过

--------------------------------------------------------------------------------------------------------------------------------------------------

  错误是难免的欢迎指正. 

       昨日重现 : http://music.163.com/m/song?id=3986241&userid=16529894

       The Carpenters - Yesterday Once[SD,854x480].mp4 : https://pan.baidu.com/s/1slA0yU5

posted on 2017-11-11 18:38  喜欢兰花山丘  阅读(946)  评论(0编辑  收藏  举报