首先介绍上一节示例中用到的一些socket编程的主要函数接口,包括函数入参、返回值。之后简单介绍跨平台下socket代码移植的处理方法。
1、socket主要函数接口
这里以linux下的函数为例说明,windows下接口类似,包含头文件有区别。本文下一节有说明。
1.1 创建套接字
#include <sys/socket.h>
int socket(int family, int type, int protocol); // 成功返回非负描述符,出错返回-1
参数family 指明协议簇,可取AF_INET、AF_INET6、AF_LOCAL等;
参数type 描述要建立的套接字的类型,如SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等;
参数protocol 指定套接字使用的特定协议,可选IPPROTO_TCP、IPPROTO_UDP,通常置为0选择默认连接方式即可。
1.2 绑定本地地址
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); //成功返回0,出错返回-1
bind()将所创建的套接字绑定到本地协议地址,协议地址是32位的IPv4地址(AF_INET)或128位的IPv6地址(AF_INET6)与16位的TCP或UDP端口的组合。
第二个参数myaddr是执行特定协议的地址指针结构,不同的协议可能导致第三个参数addrlen值(参数myaddr地址结构大小)不同。
bind()函数通常用于服务端需要调用该函数,以绑定到总所周知的的地址和端口;客户端并不需要这么做,在进行connet或者listen时内核会选择一个临时端口和ip地址。客户端可以使用getsockname获取本地协议地址信息,服务端也可以通过getpeername获取客户端的协议地址信息。
1.3 建立套接字连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); // 成功返回0,出错返回-1
参数同bind函数(),客户端与服务端建立连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *clientaddr, socklen_t* addrlen); // 成功返回非负已连接套接字,出错返回-1
使用前已调用listen()函数,服务端从已完成连接的队列中返回下一个已完成连接,如果队列为空且阻塞则进入睡眠。参数clientaddr用于表示已连接的客户端的协议地址,参数addrlen用于确定地址结构(可传入NULL,不获取客户端协议地址)。返回值为已连接的套接字描述符。
一个服务器通常只有一个监听套接字,在服务器生命周期一直存在。内核会为每个由服务器进程接受的客户连接创建一个已连接套接字(accept返回 值)。当服务器完成对某个客户的服务时,响应的已连接套接字就被关闭。
1.4 监听连接
#include <sys/socket.h>
int listen (int sockfd, int backlog); // 成功返回0,出错返回-1
该函数仅TCP服务器使用,且需要在调用socket()和bind()之后使用,表示服务器意愿从当前的sockfd上接受客户端的请求,排队请求的个数最多为backlog( 最大监听个数宏 #define SOMAXCONN 128
)。
1.5 数据传输
#include <unistd.h>
ssize_t read (int fd, void *buf, size_t n);
ssize_t write (int fd, const void *buf, size_t n);
从文件描述符fd中,读取n个字节到buf或写入buf的n个字节,成功返回实际的字节数,失败返回-1。在unix中,所有socket也当做文件,所以可以使用read或write读写socket数据。
#include <sys/socket.h>
ssize_t send (int fd, const void *buf, size_t n, int flags);
ssize_t recv (int fd, void *buf, size_t n, int flags);
比read write多了一个参数flags,可以理解为比read write操作更细化的函数,但仅用于套接字。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buff, size_t n, int flags, struct sockaddr* from, socklent_t* addr_len);
ssize_t sendto(int sockfd, void* buff, size_t n, int flags, const struct sockaddr* to, socklen_t addrlen);
前三个参数含义同函数send/recv,flags置为0,from\to 存放对端地址结构,addrlen地址结构长度。
recvfrom接收来自对端from的信息,并存于buff中;sendto发送信息buff到对端to。
一般这样使用:
socklen_t addr_len = sizeof(addr);
recvfrom(sockfd,buff,sizeof(buff),0,(struct sockaddr *)&addr,&addr_len);
(1)根据addr_len的值,判断客户端addr是地址类型是IPv4还是IPv6。
(2)对于UDP,recvfrom返回0是可以接受的;而TCP的read返回0则表示对端关闭连接。
(3)如果不关心对端的协议地址,可以将recvfrom的最后两个参数置为空指针。
(4)recvfrom和sendto都可以用于TCP,但通常不这么做。
一般在UDP套接字中使用recvfrom/sendto;在TCP套接字中使用read/write.
2.6 关闭套接字
#include <unistd.h>
int close(int sockfd)
int shutdown(int sockfd)
close()关闭套接字sockfd,并释放当前进程分配给该套接字的资源;如果涉及一个打开的TCP连接,则该连接被释放。
shutdown是一种优雅地单方向或者双方向关闭socket的方法。 而close则立即双方向强制关闭socket并释放相关资源。
如果有多个进程共享一个socket,shutdown影响所有进程,而close只影响本进程。
2、跨平台处理
主要给出主要接口函数使用下的差异,并给出跨平台下的解决方式。
2.1 主要差异
- 头文件
windows下winsock.h/winsock2.h
linux下sys/socket.h 错误处理:errno.h - 初始化、库加载
Windows下需要初始化,而linux下不需要。
程序开始需要调用初始化函数WSAStartup(),对应的退出清理用WSACleanup(); - socket类型
windows下SOCKET
linux下int - 关闭socket
windows下closesocket(…)
linux下close(…) - 获取错误码
windows下WSAGetLastError()
linux下errno变量extern int errno;
int geterror(){return errno;} - 设置非阻塞
windows下
int ul = 1
ioctlsocket(server_socket,FIONBIO,&ul);
linux下
#include <fcntl.h>
fcntl(server_socket,F_SETFL, O_NONBLOCK); - 编译链接
windows下需要链接ws2_32.lib库,inux下连接是使用参数:-lstdc。
运行时需要libstdc++.so.5,可在/usr/lib目录中创建一个链接。属于系统环境,不需要额外处理。
2.2 解决方法
解决上述跨平台之间的代码差异,可以用如下代码简单解决。
#ifdef _WIN32
#if defined(_MSC_VER)
#ifdef _WIN64
using ssize_t = __int64;
#else
using ssize_t = int;
#endif
#include <io.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#ifdef _MSC_VER
#pragma comment(lib, "ws2_32.lib")
#endif
using socket_t = SOCKET;
#else // not _WIN32
#include <sys/socket.h>
#include <netinet/in.h> // sockaddr_in, inet_addr
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h> // close,shutdown, write, read
#include <cstring.h>
#endif
通过这种方式,再编写通用的socket函数。
int close_socket(socket_t sock) {
#ifdef _WIN32
return closesocket(sock);
#else
return close(sock);
#endif
}
int shutdown_socket(socket_t sock) {
#ifdef _WIN32
return shutdown(sock, SD_BOTH);
#else
return shutdown(sock, SHUT_RDWR);
#endif
}
void set_nonblocking(socket_t sock, bool nonblocking) {
#ifdef _WIN32
auto flags = nonblocking ? 1UL : 0UL;
ioctlsocket(sock, FIONBIO, &flags);
#else
auto flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL,
nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK)));
#endif
}
bool get_error() {
#ifdef _WIN32
return WSAGetLastError;
#else
return errno;
#endif
}
源代码中添加初始化的代码
#ifdef _WIN32
class WSInit {
public:
WSInit() {
WSADATA wsaData;
WSAStartup(0x0002, &wsaData);
}
~WSInit() { WSACleanup(); }
};
static WSInit wsinit_; // 全局变量,windows下程序执行时构造初始化,退出时析构
#endif
有了以上通用代码的基础,其他的socket编程函数也能继续封装成跨平台的,那么项目代码的就具有可观的可移植性。