socket 概念
- socket 中文译名套接字, 是网络通信的基本操作单元, 可以看做是不同主机之间进程进行双向通信的端点, 即通信双方的一种约定, 可用socket的相关函数来完成通信过程
- socket是网络通信双方之间的纽带, 应用程序在网络上发送, 接受的信息都通过socket实现
- socket可建立一次连接, 并对连接唯一标识
- socket是操作系统的资源
socket 网络编程规范
- Berkeley socket
最早出现在BSD Unix中, 形成一套API - Windows socket(Winsock)
通过动态链接库的形式加入Windows操作系统
增加异步函数, 支持Windows消息驱动机制的网络事件异步选择
Windows socket 网络编程规范
Windows socket 1.1
- 库函数语法, 符号常量, 数据结构在 winsock.h
- 静态编译时用 wsock32.lib
- 动态调用时用 winsock.dll
- 继承 Berkeley socket规范
- 扩充 Berkeley socket规范, 增加了 WSA 开头的函数
- 只支持 TCP/IP 协议
windows socket 2.2
- 库函数语法, 符号常量, 数据机构在 WinSock2.h
- 静态编译时用 ws2_32.lib
- 动态调用时用 ws2_32.dll
- 支持多种协议
- 支持服务质量 QoS
- 增加异步机制和重叠I/O
- 更多的函数
socket 的类型
- stream socket (字节流套接字)
当要使用TCP协议通信时, 可使用此类型的套接字, 可提供面向连接的, 无差错的, 发送先后顺序一致的, 非重复的网络数据传输 - datagram socket (数据报套接字)
当要使用UDP协议通信时, 可使用此类型套接字, 可提供无连接的服务, 相互独立的数据传输 - raw socket (原始套接字)
提供对网络下层通信协议(如IP协议)的直接访问, 可用于开发新协议或特定网络功能
socket 网络编程思路
- 创建socket
- 将socket与地址结构绑定
- 发送/接受数据
- 释放socket
Windows socket 网络编程思路
- 初始化 Windows socket
- 创建socket
- 将socket与地址结构绑定
- 发送/接收数据
- 释放socket
- 终止Windows socket
socket常用函数和数据结构
- windows socket 的初始化和终止
- WSAStartup(
- WSACleanup(
- 创建和释放socket(
- socket(
- closesocket(
- 绑定socket和地址结构
- bind(
- listen(
- accept(
- connetc(
- sockaddr
- sockaddr_in
- in_addr
- 发送, 接收数据
- send(
- sendto(
- recv(
- recvfrom(
- 错误处理函数
- WSAGetLastError(
- 其他辅助函数
- htons(
- htonl(
- ntohs(
- ntohl(
- inet_addr(
- inet_ntoa(
- gethostbyname(
- gethostbyaddr(
- getservbyname(
初始化函数 WSAStartup(
- 用Winsock编写的网络应用程序要做的第一件事, 调用 WSAStartup( 完成初始化
- 只有初始化成功后, 才能调用其他 socket API
函数定义
int WSAAPI WSAStartup( _IN_ WORD wVersionRequested, // WinSock版本号高位字节指定副版本号, 低位字节指定主版本号, //可用宏MAKEWORD(X, Y)进行设置MAKEWORD(2, 2) _Out_ LPWSADATA lpWSAData // 返回一个WSADATA结构 ); // 返回值 // 成功: 返回0 // 失败: 返回错误代码 // WSASYSNOTREADY (网络系统未准备好) // WSAVERNOTSUPPORTED (版本不支持) // WSAEINPROGRESS (正在执行一个阻塞的操作) // WSAEFAULT (指针lpWSAData不合法)
- WSAStartup( 的功能
- 当被调用时, 找到系统目录, 根据环境变量PATH, 查找WinSock.dll
- 检查 WinSock.dll 版本号, 符号要求, 则调用成功
- 绑定应用程序与 WinSock.dll
- 函数成功返回时, 在 lpWSAData 指向的 WSADATA 结构中返回信息
- WSAData 结构保存 WSAStartup( 返回的初始化信息
WSAData 结构体定义
typedef struct WSAData { WORD wVersion; WORD wHighVersion; #ifdef _WIN64 unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; char szDescription[WSADESCRIPTION_LEN+1]; char szSystemStatus[WSASYS_STATUS_LEN+1]; #else char szDescription[WSADESCRIPTION_LEN+1]; char szSystemStatus[WSASYS_STATUS_LEN+1]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; #endif } WSADATA, FAR * LPWSADATA;
注销函数WSACleanup(
- 当网络应用程序使用完WinSock.dll, 使用 WSACleanup( 接触绑定, 终止对WinSock.dll的调用,释放引用动态链接库时占用的系统资源
函数定义
int WSAAPI WSACleanup( void );
- 函数返回值
- 成功: 返回 0
- 失败 返回 SOCKET_ERROR
- 在所有 Windows Sockets的应用程序里, WSAStartup( 和 WSACleanup(是必须使用的, 且成对使用
socket( 创建函数
定义
SOCKET WSAAPI socket( _In_ int af, _In_ int type, _In_ int protocol );
af (address family 地址族)
常量 类型 AF_UNIX Unix域socket AF_INET Internet TCP/IP AF_IPX Novell IPX AF_IRDA 红外线通信 type
常量 socket 类型 SOCK_STREAM 字节流(有连接的) SOCK_DGRAM 数据报(无连接的) SOCK_RAW 原始套接字 - protocol 表示特定的协议, 一般取 0
- 返回值
- 成功: 返回新 socket 的描述符 (无符号整数)
- 失败: 返回 INVALID_SOCKET
- 函数功能
根据指定的通信地址族, 套接字类型和通信协议创建一个新的套接字, 为它分配资源, 并返回该套接字的描述符 例子
/*创建一个数据流 socket */ SOCKET socket1 = socket(AF_INET, SOCK_STREAM, 0); /*创建一个数据报 socket */ SOCKET socket2 = socket(AF_INET, SOCK_DGRAM, 0);
closesocket( 关闭函数
- 函数的功能
关闭一个 socket, 释放 socket 描述符, 再访问时返回 WSAENOTSOCK错误 定义
int WSAAPI closesocket( _In_ SOCKET s );
- 返回值
- 成功: 返回 0
- 失败: 返回 SOCKET_ERROR
关闭 socket
- 关闭socket会关闭用户应用程序中的socket句柄, 释放相关资源, 同时会隐含的触发TCP连接的关闭过程
- TCP连接的关闭过程
- 优雅关闭 (graceful close)
如果发送缓存中还有数据未发出则其发出去, 并且收到所有数据的ACK之后, 发送FIN包, 开始关闭过程 - 强制关闭 (hard close或abortive close)
如果缓存中还有数据, 则这些数据都将被丢弃, 然后发送RST包, 直接重置TCP连接
- 优雅关闭 (graceful close)
SO_LINGER 选项的设置值决定了 closesocket( 的行为, 该选项的参数值是linger结构
struct linger { u_short l_onoff; /* option on/off */ u_short l_linger; /* linger time */ };
- l_onoff==0, 优雅关闭, socket的资源会被保留直到TCP连接关闭
- l_onoff > 0 && l_linger == 0, 强制关闭
- l_onoff > 0 && l_linger > 0, 则在l_linger表明的时间内关闭
bind( 绑定函数
- 功能
- 将 socket 绑定到指定的网络地址上, 一般在 connect( 或listen( 函数前调用
- 在服务器端, 用作监听客户端连接请求的 socket 一定要经过绑定
- 在客户端使用的套接字一般不必绑定, 除非要指定它使用特定的网络地址
定义
int WSAAPI bind( _In_ SOCKET s, _In_reads_bytes_(namelen) const struct sockaddr FAR * name, _In_ int namelen );
- 返回值
- 成功: 返回 0
- 失败: 返回 SOCKET_ERROR
sockaddr 结构体
- 功能
表示通用的 WinSock 地址结构, 针对各种通信域的套接字, 存储它们的地址信息 定义
typedef struct sockaddr { #if (_WIN32_WINNT < 0x0600) u_short sa_family; #else ADDRESS_FAMILY sa_family; // Address family. #endif //(_WIN32_WINNT < 0x0600) CHAR sa_data[14]; // Up to 14 bytes of direct address. } SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;
- sa_family
地址族: 与socket( 的第一个参数相同含义 - sa_data
协议地址, 内容因具体协议而不同
- sa_family
- 功能
sockaddr_in 结构体
typedef struct sockaddr_in { #if(_WIN32_WINNT < 0x0600) short sin_family; #else //(_WIN32_WINNT < 0x0600) ADDRESS_FAMILY sin_family; #endif //(_WIN32_WINNT < 0x0600) USHORT sin_port; IN_ADDR sin_addr; CHAR sin_zero[8]; } SOCKADDR_IN, *PSOCKADDR_IN;
- sin_family 必须为 AF_INET
- sin_port 端口号
- sin_addr IPv4 地址
- sin_zero[8] 对齐
in_addr 结构体, 存储 IP地址
typedef struct in_addr { union { struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { USHORT s_w1,s_w2; } S_un_w; ULONG S_addr; } S_un; #define s_addr S_un.S_addr /* can be used for most tcp & ip code */ #define s_host S_un.S_un_b.s_b2 // host on imp #define s_net S_un.S_un_b.s_b1 // network #define s_imp S_un.S_un_w.s_w2 // imp #define s_impno S_un.S_un_b.s_b4 // imp # #define s_lh S_un.S_un_b.s_b3 // logical host } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
- 三种地址结构的关系
listen( 监听函数
- 功能
- 适用于支持连接的 socket
- 在Internet通信域, 仅用于字节流 socket, 并仅用于服务器端。
- 监听 socket 必须已绑定在特定的网络地址上
- 监听 socket 能监听来自客户端的连接请求, 并规定了等待连接队列 (先进先出队列) 的最大长度
定义
int WSAAPI listen( _In_ SOCKET s, _In_ int backlog );
- 返回值
- 成功: 返回0
- 失败: 返回 SOCKET_ERROR
accept( 接收连接请求
- 功能
- 从监听 socket 的等待队列中抽取连接请求, 创建一个新的 socket 来与请求连接的客户端 socket 创建连接通道, 交换数据
- 如果连接成功, 返回新创建的 socket 描述符
- 若队列中没有连接请求, 当:
- 阻塞方式时, 该函数阻塞调用它的进程。
- 非阻塞方式时, 该函数返回一个错误代码
定义
SOCKET WSAAPI accept( _In_ SOCKET s, _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr, _Inout_opt_ int FAR * addrlen );
- s 服务端的监听 socket
- addr 客户端地址结构
- addrlen 客户端地址长度
- 返回值
- 成功: 新的 socket 描述符
- 失败: INVALID_SOCKET错误
connect( 请求连接函数
- 功能
客户端请求与服务器端建立连接 定义
SOCKET WSAAPI accept( _In_ SOCKET s, _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr, _Inout_opt_ int FAR * addrlen );
- s socket 若未绑定, 操作系统赋值绑定
- addr 服务器端的地址结构
- addrlen 服务器端地址长度
- 返回值
- 成功: 返回 0
- 失败: 返回 SOCKET_ERROR
send( 发送数据函数
- 功能
在已建立连接的 socket 上发送数据 定义
int WSAAPI send( _In_ SOCKET s, _In_reads_bytes_(len) const char FAR * buf, _In_ int len, _In_ int flags );
- s 已连接的 socket
- buf 待发送缓冲区
- len 待发送内容长度
- flags 选项, 一般为0
- 返回值
- 成功: 返回实际发送的字节数
- 失败: 返回SOCKET_ERROR
- 注意
- 真正向对方发送数据的过程是由协议栈完成的
- 数据报类型的套接字, 发送数据的长度不应超过子网的IP包最大长度, (最大长度在WSADATA结构中定义)
- 成功完成send( 调用不代表数据能传送到对方
recv( 接收数据函数
- 功能
在已建立连接的 socket 上接收数据 定义
int WSAAPI recv( _In_ SOCKET s, _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, _In_ int len, _In_ int flags );
- s 已连接的 socket
- buf 接受缓冲区
- len 缓冲区长度
- flags 选项, 一般为 0
- 返回值
- 成功: 返回实际接收的字节数
- 连接已终止: 0
- 失败: 返回 SOCKET_ERROR
sendto( 数据报 socket 发送数据函数
- 功能
用发送端的 socket 发送一个数据报, 数据发送由协议栈完成 定义
int WSAAPI sendto( _In_ SOCKET s, _In_reads_bytes_(len) const char FAR * buf, _In_ int len, _In_ int flags, _In_reads_bytes_(tolen) const struct sockaddr FAR * to, _In_ int tolen );
recvfrom( 数据报 socket 接收数据函数
定义
int WSAAPI recvfrom( _In_ SOCKET s, _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, _In_ int len, _In_ int flags, _Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from, _Inout_opt_ int FAR * fromlen );
WSAGetLastError( 错误捕获函数
- 调用任何一个 WinSock 函数之后, 可以用WSAGetLastError函数来获得详细的错误代码
定义
int WSAAPI WSAGetLastError( void );
- 返回值
错误代码
- WSANOTINITIALISED: 使用本API函数之前,未成功调用WSAStartup(
- WSAENETDOWN: 网络子系统失效
- WSAEADDRINUSE: 指定端口已被占用
- WSAEFAULT: name和namelen参数定义的地址结构非法
- 返回值
- 注意: WSAStartup( 调用失败不能用 WSAGetLastError( 获得错误代码。是因为还未建立存储错误信息的空间
字节序
- Big-Endian (大端) 是指低位字节排放在内存的高端, 高位字节排放在内存的低端 而Little-Endian (小端) 正好相反
- Big-Endian, Little-Endian跟多字节类型的数据有关, 比如 int, short, long型, 对byte型却没有影响
- 举例:
int a = 0x05060708
在Big-Endian的情况下存放为:
地址增长方向—>0x00|0x01|0x02|0x03
数据存储0x05|0x06|0x07|0x08
在Little-Endian的情况下存放为:
地址增长方向—> 0x00|0x01|0x02|0x03
数据存储0x08|0x07|0x06|0x05 - 主机字节顺序
Big-Endian, Little-Endian跟CPU有关, Intel架构采用Little-Endian, 而PowerPC, SPARC和Motorola架构采用Big-Endian - 网络字节顺序
指数据在网络上传输时是大端先发送还是小端先发送, Internet通信域的网络字节顺序是Big-Endian, 即无论计算机系统支持何种Endian, 在传输数据是,总是数值最高位的字节最先发送
字节序转换函数
- u_short htons( u_short hostshort )
- u_long htonl( u_long hostlong )
- u_short ntohs( u_short netshort )
- u_long ntohl( u_long netlong )
- 功能
将16bit和32bit的数据在主机字节顺序和网络字节顺序之间相互转换
在基于socket的网络应用编程中, 一般需要变换IP地址和端口号
地址转换函数
- unsigned long inet_addr( const char* cp )
将ASCII字符串地址转换成32位网络字节顺序的IP地址 - char* FAR inet_ntoa( struct in_addr in )
将32位网络字节顺序的IP地址转换成ASCII字符串地址
网络信息查询函数
- struct hostent FAR * gethostbyname( const char FAR * name )
- struct hostent FAR * gethostbyaddr( const char FAR * addr, int len, int type )
- struct servent FAR * getservbyname( const char FAR * name, const char FAR * proto )
struct hostent {
char FAR * h_name; /* official name of host */
char FAR * FAR * h_aliases; /* alias list */
short h_addrtype; /* host address type */
short h_length; /* length of address */
char FAR * FAR * h_addr_list; /* list of addresses */
#define h_addr h_addr_list[0] /* address, for backward compat */
};
struct servent {
char FAR * s_name; /* official service name */
char FAR * FAR * s_aliases; /* alias list */
#ifdef _WIN64
char FAR * s_proto; /* protocol to use */
short s_port; /* port # */
#else
short s_port; /* port # */
char FAR * s_proto; /* protocol to use */
#endif
};
示例约定
使用 Microsoft Visual Studio Community 2017
去掉 stdafx.h 和 stdafx.cpp, 不使用 c++
由于VS编码问题, 只使用 ASCII 字符
由于会用到一些老旧的函数, 在文件开头定义常量
#define _WINS