CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章高频C++11重难点知识解析

  • 第2章Linux GDB高级调试指南

  • 第3章C++多线程编程从入门到进阶

  • 第4章C++网络编程重难点解析

    • 4.1 学习网络编程,你应该掌握哪些 socket 函数
    • 4.2 TCP 网络通信的基本流程
    • 4.3 设计跨平台网络通信库时需要注意的一些 socket 函数用法
      • 4.4 bind 函数重难点分析
      • 4.5 select 函数用法和原理
      • 4.6 socket 的阻塞模式和非阻塞模式
      • 4.7 发送 0 字节的数据是什么效果?
      • 4.8 connect 函数在阻塞和非阻塞模式下的行为
      • 4.9 连接时顺便接收第一组数据
      • 4.10 如何获取当前 socket 对应的接收缓冲区中有多少数据可读
      • 4.11 Linux EINTR 错误码
      • 4.12 Linux SIGPIPE 信号
      • 4.13 Linux poll 函数用法
      • 4.14 Linux epoll 模型
      • 4.15 高效的 readv 和 writev 函数
      • 4.16 主机字节序和网络字节序
      • 4.17 域名解析 API 介绍
    • 第5章网络通信故障排查常用命令

    • 第6章高性能网络通信协议设计精要

    • 第7章高性能服务结构设计

    • 第8章Redis 网络通信模块源码分析

    • 第9章后端服务重要模块设计探索

    • C++后端开发进阶
    • 第4章C++网络编程重难点解析
    zhangxf
    2023-04-05
    目录

    4.3 设计跨平台网络通信库时需要注意的一些 socket 函数用法

    这里说的跨平台,指的是在 Windows 和 Linux 使用相关 socket 函数,虽然二者的设计均参考 Berkeley Sockets,但是二者在演化的过程中还是存在不少区别,本节为读者整理了一些常用的注意事项。

    # 4.3.1 socket 数据类型

    Windows 上一个 socket 对象其类型是 SOCKET,这是一个句柄对象(本质上也是一个 int 类型),Linux 上一个 socket 对象是 int 类型,习惯上在 Windows 上称为“socket”,在 Linux 上称为 fd。所以在很多网络库中使用如下定义来包裹出一个跨平台使用的 socket 类型:

    #ifdef WIN32
    typedef SOCKET SOCKET_TYPE;
    #else
    typedef int    SOCKET_TYPE;
    #endif
    
    1
    2
    3
    4
    5

    这样就可以同时在 Windows 和 Linux 使用 SOCKET_TYPE 这个类型代表 socket 了。

    无论是 Windows 还是 Linux,创建一个套接字的函数 socket() 调用失败均会返回 -1,Windows 为这种情形定义了一个宏:

    #define INVALID_HANDLE_VALUE (-1)
    
    1

    Linux 不存在,我们可以自己通过上述语句定义之。

    # 4.3.2 Windows 系统调用相关 socket 函数需要先初始化 socket 相关 dll 文件

    Linux 程序可以直接使用相关 socket 函数,但是对于 Windows 平台必须先调用 WSAStartup() 函数显式将与 socket 相关的 dll 加载到进程地址空间来,程序退出时,需要调用 WSACleanup() 卸载相关 dll。

    这两个函数的用法示例如下:

    //程序初始化时调用
    bool InitSocket()
    {
    	//指定版本号
    	WORD wVersionRequested = MAKEWORD(2, 2);
    	WSADATA wsaData;
    	int nErrorID = ::WSAStartup(wVersionRequested, &wsaData);
    	if(nErrorID != 0)
    		return false;
    
        if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
        {
            UnInitSocket();
            return false;
        }
    
    	return TRUE;
    }
    
    //程序退出时调用
    void UnInitSocket()
    {
    	::WSACleanup();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    需要注意的是 WSAStartup() 和 WSACleanup() 是进程相关的,任何一个线程都可以调用,对于 WSAStartup() 函数某个线程调用一次之后,其他线程就可以正常使用了;反过来,如果某个线程自己不再使用相关 socket 函数,如果其调用 WSACleanup() 函数,会导致其他线程无法继续使用相关 socket 函数。因为,你调用 WSACleanup() 函数之前应该确保整个程序不再有使用 socket 函数的地方。鉴于此,一般在进程退出时才调用该函数。

    # 4.3.3 关闭 socket 函数

    由于历史原因,Windows 从来没有以良好兼容的方式实现 Berkeley 套接字 API。关闭套接字的接口,在 Linux(Unix) 中使用 close() 函数,在Windows 中使用 **closesocket() ** 函数,问题是 Windows 也定义了一个 close() 函数,且该函数还不能用于关闭 socket。如果调用该函数关闭 socket,会导致程序崩溃。这是让很多开发者容易犯错的地方。我们可以这样进行包装一下:

    #ifndef WIN32
    #define closesocket(s) close(s)
    #endif
    
    1
    2
    3

    这样我们在 Linux 平台也可以使用 closesocket 来关闭 socket 了。

    # 4.3.4 获取 socket 函数错误码

    如果某个 socket 函数调用失败,Windows 上需要调用 WSAGetLastError() 获取错误码,Linux 直接使用 errno 变量获取错误码,所以你可能会在一些网络库中看到如下代码:

    #ifdef WIN32
    #define GetSocketError() ::WSAGetLastError();
    #else
    #define GetSocketError() errno;
    #endif
    
    1
    2
    3
    4
    5

    这样就可以使用 GetSocketError() 函数统一进行处理了。

    我们看下 libevent 网络库在这一块的相关实现。

    //Windows上获取一个socket错误码的实现
    #ifdef _WIN32
    int evutil_socket_geterror(evutil_socket_t sock)
    {
    	int optval, optvallen=sizeof(optval);
    	int err = WSAGetLastError();
    	if (err == WSAEWOULDBLOCK && sock >= 0) {
    		//不仅使用了WSAGetLastError,还通过 getsocketopt 选项来获取错误
    		if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (void*)&optval,
    					   &optvallen))
    			return err;
    		if (optval)
    			return optval;
    	}
    	return err;
    }
    #endif
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //Linux 上设置错误码
    #define EVUTIL_SET_SOCKET_ERROR(errcode)		\
    		do { errno = (errcode); } while (0)
    
    1
    2
    3
    //Windows上把错误码转换成错误描述信息的函数实现
    /** Equivalent to strerror, but for windows socket errors. */
    const char *evutil_socket_error_to_string(int errcode)
    {
    	struct cached_sock_errs_entry *errs, *newerr, find;
    	char *msg = NULL;
    
    	EVLOCK_LOCK(windows_socket_errors_lock_, 0);
    
    	find.code = errcode;
    	errs = HT_FIND(cached_sock_errs_map, &windows_socket_errors, &find);
    	if (errs) {
    		msg = errs->msg;
    		goto done;
    	}
    
    	if (0 != FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
    			       FORMAT_MESSAGE_IGNORE_INSERTS |
    			       FORMAT_MESSAGE_ALLOCATE_BUFFER,
    			       NULL, errcode, 0, (char *)&msg, 0, NULL))
    		chomp (msg);	/* because message has trailing newline */
    	else {
    		size_t len = 50;
    		/* use LocalAlloc because FormatMessage does */
    		msg = LocalAlloc(LMEM_FIXED, len);
    		if (!msg) {
    			msg = (char *)"LocalAlloc failed during Winsock error";
    			goto done;
    		}
    		evutil_snprintf(msg, len, "winsock error 0x%08x", errcode);
    	}
    
    	newerr = (struct cached_sock_errs_entry *)
    		mm_malloc(sizeof (struct cached_sock_errs_entry));
    
    	if (!newerr) {
    		LocalFree(msg);
    		msg = (char *)"malloc failed during Winsock error";
    		goto done;
    	}
    
    	newerr->code = errcode;
    	newerr->msg = msg;
    	HT_INSERT(cached_sock_errs_map, &windows_socket_errors, newerr);
    
     done:
    	EVLOCK_UNLOCK(windows_socket_errors_lock_, 0);
    
    	return msg;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    //Linux 上就比较简单了,直接调用strerror函数传入错误码即可
    #define evutil_socket_error_to_string(errcode) (strerror(errcode))
    
    1
    2

    libevent 提供了这些宏访问和操作 socket 错误代码,evutil_socket_geterror() 则返回某特定套接字的错误号,在类 Unix 系统中 EVUTIL_SET_SOCKET_ERROR() 修改当前套接字错误号,evutil_socket_error_to_string() 返回代表某给定套接字错误号的字符串。

    # 4.3.5 套接字函数返回值

    无论是 Windows 还是 Linux,大多数 socket 函数调用失败后会返回 -1,在 Windows 系统上为这种情形专门定义了一个宏 SOCKET_ERROR。

    #define SOCKET_ERROR (-1)
    
    1

    可以为 Linux 上也定义这样一个宏,这样就方便统一书写了:

    #ifndef WIN32
    #define SOCKET_ERROR (-1)
    #endif
    
    1
    2
    3

    # 4.3.6 select 函数第一个参数问题

    select 函数的原型是:

    int select(int nfds, 
       	   fd_set *readfds,
              fd_set *writefds,
              fd_set *exceptfds,
              struct timeval *timeout);
    
    1
    2
    3
    4
    5

    使用示例:

    fd_set writeset;
    FD_ZERO(&writeset);
    FD_SET(m_hSocket, &writeset);
    struct timeval tv;
    tv.tv_sec = 3;
    tv.tv_usec = 100;
    select(m_hSocket + 1, NULL, &writeset, NULL, &tv);
    
    1
    2
    3
    4
    5
    6
    7

    无论是 Windows 还是 Linux,这个函数的 readfds、writefds 和 exceptfds 参数都是一个包含一组 socket 描述符数组的结构体。在 Linux 下,第一个参数必须设置成这三个参数中,所有 socket 描述符句柄中的最大值加 1;但是 Windows 的 select 函数不使用第一个参数,可以随意设置,纯粹是为了保持与 Berkeley 套接字兼容才保留了这个参数。一般为了兼容,在 Windows 上也会将这个参数的值设置为这三个 fd_set 集合中最大套接字值加 1。

    # 4.3.7 关于错误码 WSAEWOULDBLOCK 和 EWOULDBLOCK

    对于某些套接字函数操作不能立即完成,Windows 上的错误码 WSAEWOULDBLOCK, 在 Linux 上对应 EWOULDBLOCK(Linux 还存在一个 EAGAIN 错误码与此同义)。

    为了统一写法,我们可以在 Windows 上使用如下代码来兼容:

    #ifdef WIN32
    #define EWOULDBLOCK WSAEWOULDBLOCK
    #endif
    
    1
    2
    3

    其他套接字函数

    对于 IO 复用函数,Windows 和 Linux 都支持 select 函数,此外 Linux 有特有的 poll、epoll 模型,Windows 有 WSAPoll 函数和完成端口模型(IOCP)。

    另外,Windows 和 Linux 上有许多自己特有的函数和网络通信模型,Windows 自我扩展的套接字函数一般以 WSA 开头(Windows Socket API 的缩写),例如 WSASend、WSARecv等等,Windows 上提供了方便的 WSAEventSelect 模式和 WSAAsyncSelect 等网络通信模型。Linux 上有 accept4、socketpair 等方便的函数。总之,二者的发展也是相互借鉴,无所谓孰优孰劣。

    此小节对 Windows 和 Linux 套接字函数的一些重要区别和特性做了一些归纳性总结,后面章节会针对性地对相关内容进行介绍。

    上次更新: 2025/04/01, 20:53:14
    4.2 TCP 网络通信的基本流程
    4.4 bind 函数重难点分析

    ← 4.2 TCP 网络通信的基本流程 4.4 bind 函数重难点分析→

    最近更新
    01
    第二章 关键字static及其不同用法
    03-27
    02
    第一章 auto与类型推导
    03-27
    03
    C++语言面试问题集锦 目录与说明
    03-27
    更多文章>
    Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式