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,854×480].mp4 :
https://pan.baidu.com/s/1slA0yU5