1. 程式人生 > >socket 開發 - 那些年用過的基礎 API

socket 開發 - 那些年用過的基礎 API

ops calendar 多少 type eva igp 方法 system 普通

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

前言 - 思考還是

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

  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

socket 開發 - 那些年用過的基礎 API