Linux/Window下socket

来源:互联网 发布:上海网页美工培训班 编辑:程序博客网 时间:2024/04/30 12:03

http://c.biancheng.net/cpp/html/3032.html,非常好的学习网站,转载到这为了方便学习

在Linux中,一切都是文件,除了文本文件、源文件、二进制文件等,一个硬件设备也可以被映射为一个虚拟的文件,称为设备文件。例如,stdin 称为标准输入文件,它对应的硬件设备一般是键盘,stdout 称为标准输出文件,它对应的硬件设备一般是显示器。对于所有的文件,都可以使用 read() 函数读取数据,使用 write() 函数写入数据。

“一切都是文件”的思想极大地简化了程序员的理解和操作,使得对硬件设备的处理就像普通文件一样。所有在Linux中创建的文件都有一个 int 类型的编号,称为文件描述符(File Descriptor)。使用文件时,只要知道文件描述符就可以。例如,stdin 的描述符为 0,stdout 的描述符为 1。

在Linux中,socket 也被认为是文件的一种,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用与文件 I/O 相关的函数。可以认为,两台计算机之间的通信,实际上是两个 socket 文件的相互读写。

文件描述符有时也被称为文件句柄(File Handle),但“句柄”主要是 Windows 中术语,所以本教程中如果涉及到 Windows 平台将使用“句柄”,如果涉及到 Linux 平台将使用“描述符”。


在Linux下创建 socket
在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

int socket(int af, int type, int protocol);
1) 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。
2) type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM,在《socket是什么意思》一节中已经进行了介绍。
3) protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?

正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

该教程使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议
这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() 函数:
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示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 socket(int af, int type, int protocol);
除了返回值类型不同,其他都是相同的。Windows 不把套接字作为普通文件对待,而是返回 SOCKET 类型的句柄。请看下面的例子:
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;而客户端要用 connect() 函数建立连接。


bind() 函数
bind() 函数的原型为:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linuxint bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows
下面以Linux为例进行讲解,Windows与此类似。
sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

下面的代码,将创建的套接字与IP地址 127.0.0.1、端口 1234 绑定:

//创建套接字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、端口绑定bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

这里我们使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型,后边会讲解为什么这样做。


sockaddr_in 结构体
接下来不妨先看一下 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填充};
1) sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。
2) sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。
端口号需要用 htons() 函数转换,后面会讲解为什么。
3) sin_addr 是 struct in_addr 结构体类型的变量,下面会详细讲解。
4) sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。

in_addr 结构体
sockaddr_in 的第3个成员是 in_addr 类型的结构体,该结构体只包含一个成员,如下所示:

struct in_addr{    in_addr_t  s_addr;  //32位的IP地址};
in_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换,例如:
unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);
运行结果:
16777343


图解 sockaddr_in 结构体
为什么要搞这么复杂,结构体中嵌套结构体,而不用 sockaddr_in 的一个成员变量来指明IP地址呢?socket() 函数的第一个参数已经指明了地址类型,为什么在 sockaddr_in 结构体中还要再说明一次呢,这不是啰嗦吗?
这些繁琐的细节确实给初学者带来了一定的障碍,我想,这或许是历史原因吧,后面的接口总要兼容前面的代码。各位读者一定要有耐心,暂时不理解没有关系,根据教程中的代码“照猫画虎”即可,时间久了自然会接受。
为什么使用 sockaddr_in 而不使用 sockaddr
bind() 第二个参数的类型为 sockaddr,而代码中却使用 sockaddr_in,然后再强制转换为 sockaddr,这是为什么呢?

sockaddr 结构体的定义如下:
struct sockaddr{    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型    char         sa_data[14];  //IP地址和端口号};

下图是 sockaddr 与 sockaddr_in 的对比(括号中的数字表示所占用的字节数):

sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。

可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址,它的定义如下:
struct sockaddr_in6 {     sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6    in_port_t sin6_port;  //(2)16位端口号    uint32_t sin6_flowinfo;  //(4)IPv6流信息    struct in6_addr sin6_addr;  //(4)具体的IPv6地址    uint32_t sin6_scope_id;  //(4)接口范围ID};

正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

connect() 函数
connect() 函数用来建立连接,它的原型为:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linuxint connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);  //Windows
各个参数的说明和 bind() 相同,不再赘述。
对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

listen() 函数
通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:
int listen(int sock, int backlog);  //Linux
int listen(SOCKET sock, int backlog);  //Windows
sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。

所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。

如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。


accept() 函数
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //LinuxSOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);  //Windows
它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

前面我们说过,两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。

write() 的原型为
ssize_t write(int fd, const void *buf, size_t nbytes);
fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 "size_t" 前面加了一个"s",代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。
write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。


read() 的原型为:
ssize_t read(int fd, void *buf, size_t nbytes);
fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。

read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。
Windows下数据的接收和发送

Windows 和 Linux 不同,Windows 区分普通文件和套接字,并定义了专门的接收和发送的函数。

从服务器端发送数据使用 send() 函数,它的原型为:
int send(SOCKET sock, const char *buf, int len, int flags);

sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项。
返回值和前三个参数不再赘述,最后的 flags 参数一般设置为 0 或 NULL,初学者不必深究。

在客户端接收数据使用 recv() 函数,它的原型为:
int recv(SOCKET sock, char *buf, int len, int flags);


window部分补充:

WSAStartup()函数以及DLL的加载
WinSock(Windows Socket)编程依赖于系统提供的动态链接库(DLL),有两个版本:
较早的DLL是 wsock32.dll,大小为 28KB,对应的头文件为 winsock1.h;
最新的DLL是 ws2_32.dll,大小为 69KB,对应的头文件为 winsock2.h。

几乎所有的 Windows 操作系统都已经支持 ws2_32.dll,包括个人操作系统 Windows 95 OSR2、Windows 98、Windows Me、Windows 2000、XP、Vista、Win7、Win8、Win10 以及服务器操作系统 Windows NT 4.0 SP4、Windows Server 2003、Windows Server 2008 等,所以你可以毫不犹豫地使用最新的 ws2_32.dll。

使用DLL之前必须把DLL加载到当前程序,你可以在编译时加载,也可以在程序运行时加载,《C语言高级教程》中讲到了这两种加载方式,请猛击:动态链接库DLL的加载:隐式加载(载入时加载)和显式加载(运行时加载)。

这里使用#pragma命令,在编译时加载:

#pragma comment (lib, "ws2_32.lib")

WSAStartup() 函数

使用DLL之前,还需要调用 WSAStartup() 函数进行初始化,以指明 WinSock 规范的版本,它的原型为:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

wVersionRequested 为 WinSock 规范的版本号,低字节为主版本号,高字节为副版本号(修正版本号);lpWSAData 为指向 WSAData 结构体的指针。
关于 WinSock 规范

WinSock 规范的最新版本号为 2.2,较早的有 2.1、2.0、1.1、1.0,ws2_32.dll 支持所有的规范,而 wsock32.dll 仅支持 1.0 和 1.1。

wsock32.dll 已经能够很好的支持 TCP/IP 通信程序的开发,ws2_32.dll 主要增加了对其他协议的支持,不过建议使用最新的 2.2 版本。

wVersionRequested 参数用来指明我们希望使用的版本号,它的类型为 WORD,等价于 unsigned short,是一个整数,所以需要用 MAKEWORD() 宏函数对版本号进行转换。例如:
MAKEWORD(1, 2);  //主版本号为1,副版本号为2,返回 0x0201
MAKEWORD(2, 2);  //主版本号为2,副版本号为2,返回 0x0202


关于 WSAData 结构体
WSAStartup() 函数执行成功后,会将与 ws2_32.dll 有关的信息写入 WSAData 结构体变量。WSAData 的定义如下:

typedef struct WSAData {    WORD           wVersion;  //ws2_32.dll 建议我们使用的版本号    WORD           wHighVersion;  //ws2_32.dll 支持的最高版本号    //一个以 null 结尾的字符串,用来说明 ws2_32.dll 的实现以及厂商信息    char           szDescription[WSADESCRIPTION_LEN+1];    //一个以 null 结尾的字符串,用来说明 ws2_32.dll 的状态以及配置信息    char           szSystemStatus[WSASYS_STATUS_LEN+1];    unsigned short iMaxSockets;  //2.0以后不再使用    unsigned short iMaxUdpDg;  //2.0以后不再使用    char FAR       *lpVendorInfo;  //2.0以后不再使用} WSADATA, *LPWSADATA;

最后3个成员已弃之不用,szDescription 和 szSystemStatus 包含的信息基本没有实用价值,读者只需关注前两个成员即可。请看下面的代码:
#include <stdio.h>#include <winsock2.h>#pragma comment (lib, "ws2_32.lib")int main(){    WSADATA wsaData;    WSAStartup( MAKEWORD(2, 2), &wsaData);    printf("wVersion: %d.%d\n", LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion));    printf("wHighVersion: %d.%d\n", LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));    printf("szDescription: %s\n", wsaData.szDescription);    printf("szSystemStatus: %s\n", wsaData.szSystemStatus);    return 0;}

运行结果:
wVersion: 2.2
wHighVersion: 2.2
szDescription: WinSock 2.0
szSystemStatus: Running

ws2_32.dll 支持的最高版本为 2.2,建议使用的版本也是 2.2。
综上所述:WinSock 编程的第一步就是加载 ws2_32.dll,然后调用 WSAStartup() 函数进行初始化,并指明要使用的版本号。


Linux

服务器端代码 server.cpp:

#include <stdio.h>#include <string.h>#include <stdlib.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>#include <netinet/in.h>int main(){    //创建套接字    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);    //将套接字和IP、端口绑定    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);  //端口    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));    //进入监听状态,等待用户发起请求    listen(serv_sock, 20);    //接收客户端请求    struct sockaddr_in clnt_addr;    socklen_t clnt_addr_size = sizeof(clnt_addr);    int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);    //向客户端发送数据    char str[] = "Hello World!";    write(clnt_sock, str, sizeof(str));       //关闭套接字    close(clnt_sock);    close(serv_sock);    return 0;}

客户端代码 client.cpp:

#include <stdio.h>#include <string.h>#include <stdlib.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>int main(){    //创建套接字    int sock = socket(AF_INET, SOCK_STREAM, 0);    //向服务器(特定的IP和端口)发起请求    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);  //端口    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));       //读取服务器传回的数据    char buffer[40];    read(sock, buffer, sizeof(buffer)-1);       printf("Message form server: %s\n", buffer);       //关闭套接字    close(sock);    return 0;}


Window

服务器端代码 server.cpp:

#include <stdio.h>#include <winsock2.h>#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dllint main(){    //初始化 DLL    WSADATA wsaData;    WSAStartup( MAKEWORD(2, 2), &wsaData);    //创建套接字    SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    //绑定套接字    sockaddr_in sockAddr;    memset(&sockAddr, 0, sizeof(sockAddr));  //每个字节都用0填充    sockAddr.sin_family = PF_INET;  //使用IPv4地址    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址    sockAddr.sin_port = htons(1234);  //端口    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));    //进入监听状态    listen(servSock, 20);    //接收客户端请求    SOCKADDR clntAddr;    int nSize = sizeof(SOCKADDR);    SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);    //向客户端发送数据    char *str = "Hello World!";    send(clntSock, str, strlen(str)+sizeof(char), NULL);    //关闭套接字    closesocket(clntSock);    closesocket(servSock);    //终止 DLL 的使用    WSACleanup();    return 0;}

客户端代码 client.cpp:

#include <stdio.h>#include <stdlib.h>#include <WinSock2.h>#pragma comment(lib, "ws2_32.lib")  //加载 ws2_32.dllint main(){    //初始化DLL    WSADATA wsaData;    WSAStartup(MAKEWORD(2, 2), &wsaData);    //创建套接字    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    //向服务器发起请求    sockaddr_in sockAddr;    memset(&sockAddr, 0, sizeof(sockAddr));  //每个字节都用0填充    sockAddr.sin_family = PF_INET;    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");    sockAddr.sin_port = htons(1234);    connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));    //接收服务器传回的数据    char szBuffer[MAXBYTE] = {0};    recv(sock, szBuffer, MAXBYTE, NULL);    //输出接收到的数据    printf("Message form server: %s\n", szBuffer);    //关闭套接字    closesocket(sock);    //终止使用 DLL    WSACleanup();    system("pause");    return 0;}


0 0