本文参考了http://c.biancheng.net/view/2123.html
windows下socket的API和linux下的API大致相同,只是在某些细节上有些细微的差别。
//与套接字相关的函数声明和结构体定义,如socket()、bind()、connect()及struct sockaddr的定义等
//primitive system data types(包含很多类型重定义,如pid_t、int8_t等)
//某些结构体声明、宏定义,如struct sockaddr_in、PROTO_ICMP、INADDR_ANY等
//某些函数声明,如inet_ntop()、inet_ntoa()等
//I/O控制操作相关的函数声明,如ioctl()
//某些结构体定义和宏定义,如EXIT_FAILURE、EXIT_SUCCESS等
//某些结构体定义、宏定义和函数声明,如struct hostent、struct servent、gethostbyname()、gethostbyaddr()、herror()等
WinSock(Windows Socket)编程依赖于系统提供的动态链接库(DLL),有两个版本:
使用 DLL 之前必须把 DLL 加载到当前程序,你可以在编译时加载,也可以在程序运行时加载,这里使用#pragma命令,在编译时加载;
#include
#pragma comment (lib, "ws2_32.lib")
在函数主体中使用相关API之前需要进行初始化;
WSAData wsaData;
if(0 != WSAStartup(MAKEWORD(2,2),&wsaData))
printf("初始化失败!%d\n",WSAGetLastError());
socket()是用来创建套接字的,Linux中socket函数原型为:
int socket(int af, int type, int protocol);
- af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
大家需要记住127.0.0.1,它是一个特殊IP地址,表示本机地址,后面的教程会经常用到。
你也可以使用 PF 前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。- type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字),我们已经在《套接字有哪些类型》一节中进行了介绍。
- protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
linux系统提供的socket()函数返回一个int整型,这个整型也就是一个FD文件描述符;
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP协议
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP协议
上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
windows系统提供的socket函数参数和用法相同,只是返回值是windows系统定义的SOCKET类型的句柄。
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
bind()函数的功能是将一个套接字socket与特定的IP地址和端口绑定。
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen); //Windows
sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出,返回值为一个int整型,可以根据返回值判断是否绑定成功。
下面给出一个示例,如何将套接字绑定到一个IP地址和端口:
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//将套接字和IP、端口绑定
if(SOCKET_ERROR ==bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)))
printf("bind failed!%d\n",WSAGetLastError());
可以看到这里定义了一个sockaddr_in结构体来保存协议族、IP地址以及端口号,我们可以看一下sockaddr_in结构体的定义:
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
至于为什么要使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型,其实这是一个历史遗留问题,后来的接口总是得考虑兼容之前的代码,所以这样的操作看似繁琐,却很有必要。其实可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。
另外绑定IP地址和绑定端口的时候有时候会用到htonl() 函数和htons() 函数,这个涉及到网络数据大小端的问题:
在使用具体的IP地址的使用,直接传入字符串则不需大小端转换,直接使用inet_addr(char*)函数来绑定,如果要使用INADDR_ANY(多网卡IP地址绑定)则需要htonl(INADDR_ANY)来转换,或者要通过一个端口号来绑定套接字的端口号,那么则需要htos(int)来转换。
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //本地所有网卡的IP地址
serv_addr.sin_port = htons(8888); //端口
connect函数用于客户端,将一个流类型套接字(TCP协议套接字)与服务端建立连接。
connect函数各个参数的含义与bind()函数完全一样
int connect(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
int connect(SOCKET sock, const struct sockaddr *addr, int addrlen); //Windows
connect()调用之后线程会进入阻塞状态,直至成功连接服务端。
listen函数可以让服务端的一个套接字进入被动监听的状态,这个套接字我们称它为监听套接字。
int listen(int sock, int backlog); //Linux
int listen(SOCKET sock, int backlog); //Windows
sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
可以将backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般较大,可能为几百或者上千。
需要注意的是,listen()函数只是让套接字进入一种监听的状态,并没有真正接受请求,接受请求需要调用accept()函数。
当监听套接字处于监听状态时,调用accept()函数可以接受客户端的请求。
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Windows
它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。accept()返回一个套接字,这个返回的套接字才是与客户端建立连接的套接字,而不是之前的监听套接字。
和connect类似,accept()也会阻塞线程,直至接受到请求。
int send(SOCKET sock, const char *buf, int len, int flags); //发送
int recv(SOCKET sock, char *buf, int len, int flags); //接受
sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项,一般将flags置为0或者NULL。
ssize_t write(int fd, const void *buf, size_t nbytes); //发送
ssize_t read(int fd, void *buf, size_t nbytes); //接受
fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。
调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。
int shutdown(int sock, int howto); //Linux
int shutdown(SOCKET s, int howto); //Windows
sock 为需要断开的套接字,howto 为断开方式。
howto 在 Linux 下有以下取值:
SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。
howto 在 Windows 下有以下取值:
SD_RECEIVE:关闭接收操作,也就是断开输入流。
SD_SEND:关闭发送操作,也就是断开输出流。
SD_BOTH:同时关闭接收和发送操作。
确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。