在上一章中我们认识并实践了创建套接字的过程。接下来我们要更为细致来探索创建套接字时的各种细节。
我们会通过以下函数来创建套接字:
#include<sys/socket.h>
int socket(int domain,int type,int protocol);//成功时返回文件描述符,失败时返回-1
domain //套接字中使用的协议族信息
type //套接字数据传输类型信息
protocol//计算机间通信中使用的协议信息
对于以上参数可能会记不住的问题,实际上通过理解一下即可。在上一章中我们说到过,套接字时是际网络通信的开始,在操作系统中,它通常以文件的抽象形式出现的。那么既然是网络连接,它就一定符合TCP/IP协议栈。那么既然是网络连接的开始,我们肯定是要指明所用的传输层,网络层和链路层,不然接下来的数据通信就不知道从何开始了。
协议族(Protocol Family)
头文件sys/socket.h中声明的协议族
名称 | 协议族 |
PF_INET | IPv4互联网协议族 |
PF_INET6 | IPv6互联网协议族 |
PF_LOCAL | 本地通信的UNIX协议族 |
PF_PACKET | 底层套接字的协议族 |
PF_IPX | IPXNovell协议族 |
另外,套接字中实际采用的最终协议信息是通过socket函数的第三个参数传递的。在指定的协议族范围内通过第一个参数决定第三个参数。
套接字类型(Type)
学过TCP/IP协议栈的人可以这样理解出现套接字类型这个参数的原因。如果第一个参数定义了网络层的内容,那么传输层的内容肯定也要被定义的。进一步的,决定了协议族并不能同时决定数据传输方式,换而言之,socket函数第一个参数PF_INET协议族中也存在多种数据传输方式。
套接字类型1:面向连接的套接字(SOCK_STREAM)
特点1:传输数据的过程中数据不会消失
特点2:按序传输数据
特点3:传输的数据不存在数据边界
特点4:套接字连接必须一一对应
由于套接字类型的确定可以理解为传输层类型的确定,那么面向连接可以理解为采用TCP的运输层协议,那么上述的特点可以通过TCP的特点进行记忆。
可能有点难以理解第三个特点,举个例子即可,"传输数据的计算机通过三次调用write函数传递了100字节的数据,但是接收数据的计算机仅通过1次read函数调用就接受了全部100个字节",也就是说,在面向连接的套接字中,read函数和write函数的调用次数并无太大影响。
一句话概括上述特点:可靠的,按序传递的,基于字节的面向连接的数据传输方式的套接字。
套接字类型2:面向消息的套接字(SOCK_DGRAM)
特点1:强调快速传输而非传输顺序
特点2:传输的数据可能丢失也可能销毁
特点3:传输的数据有数据边界
特点4:限制每次传输的数据大小
上述的特点也是UDP协议的特点。特点3的含义就是一次write对应着一次read。
面向消息的套接字特性总结如下:不可靠的,不按序传递的,以数据的高速传递为目的的套接字。
协议的最终选择
第三个参数的存在在某些情况下可有可无。它的作用是在第一个参数和第二个参数确定了后如果依然确定不了套接字的具体类型,那么此时它的作用就是最终确定协议。以前面讲过的内容为基础,如果我要创建一个"IPv4协议族中面向连接的套接字",参数PF_INET指IPv4网络协议族,SOCK_STREAM是面向连接的数据传输。满足这两个条件的协议只有IPPROTO_TCP,因此可以如下调用socket函数创建套接字,这种套接字称为TCP套接字。
int tcp_socket=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)
在大部分的情况下可以向第三个参数传递0.
面向连接的套接字:TCP套接字示例
在上一章中服务器和客户端都是基于TCP套接字的示例,现在调整其中的一部分代码,以验证TCP套接字的第三个特性。为了验证这一点,需要让write函数的调用次数不同于read函数的调用次数。因此,在客户端中分多次调用read函数以接收服务器端发送的全部数据。
"头文件和上一章相同,故省略"
void error_handling(char*message);
int main(int argc,char*argv[]){
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len=0;
itn idx=0,read_len=0;
if(argc!=3){
printf("Usage : %s <IP> <port>\n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,0);
if(sock==-1)error_handling("socket() error");
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serc_addr,sizeof(serv_addr))==-1);
error_handling("connect() error!");
while(read_len=read(sock,&message[idx++],1)){
if(read_len==-1)error_handling("read() error!");
str_len+=read_len;
}
printf("Message from server: %s \n",message);
printf("Function read call count: %d \n",str_len);
close(sock);
return 0;
}
error_handling(char*message){
//与上一章代码相似
}
根据运行结果,服务器端调用了1次write函数传递了13个字节的内容,而客户端调用13次read函数读取13个字节的内容。
Windows 平台下的实现及验证
前面讲过的套接字类型及传输特性与操作系统无关。Windows平台下的实现方式也类似,不
需要过多说明,只需稍加了解socket函数返回类型即可。
#include <winsock2.h>
SOCKET socket(int af, int type, int protocol); //成功时返回 socket 句柄,失败时返回 INVALID_SOCKET
该函数的参数种类及含义与Linux的socket函数完全相同,故省略,只讨论返回值类型。可以看出返回值类型为SOCKET,此结构体用来保存整数型套接字句柄值。实际上,socket函数返回整数型数据,因此可以通过int型变量接收,就像在Linux中做的一样。但考虑到以后的扩展性,定义为SOCKET数据类型,希望各位也使用SOCKET结构体变量保存套接字句柄,这也是微软希
望看到的。以后即可将SOCKET视作保存套接字句柄的一个数据类型。
同样,发生错误时返回INVALID_SOCKET,只需将其理解为提示错误的常数即可。其实际
值为-1,但值是否为-1并不重要,除非编写如下代码。
SOCKET soc = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(soc ==-1 )
ErrorHandling("...");
如果这样编写代码,那么微软定义的INVALID_SOCKET常数将失去意义!应该如下编写,
SOCKET soc = socket(PF_INET, SOCK_STREAM,IPPROTO_TCP);
if ( soc == INVALID_SOCKET )
ErrorHandling("....");
基于Windows的TCP套接字演示
和之前一样,只给出客户端的源代码及运行结果。
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
void ErrorHnadling(char*message);
int main(int argc,char*argv[]){
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strlen=0;
int idx=0,readLen=0;
if(argc!=3){
printf("Usage : %s <IP> <port>\n",argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)ErrorHandling("WSAStartup() error!");
hSocket=socket(PF_INET,SOCK_STREAM,0);
if(hSocket==INVALID_SOCKET)ErrorHandling("hSocket() error");
memset(&servAddr,0,sizeof(servAddr));
servAddr.sin_family=AF_INET;
servAddr.sin_addr.s_addr=inet_addr(argv[1]);
setvAddr.sin_port=htons(atoi(argv[2]));
if(connect(hSocket,(SOCKADDR*)&servAddr,sizeof(servAddr))==SOCKET_ERROR)
ErrorHandling("connect() error!");
while(readLen=recv(hSocket,&message[idx++],1,0)){
if(readLen==-1)ErrorHandling("read() error!");
strLen+=readLne;if(message[idx-1]=='\0')break;
}
printf("Message from server: %s \n",message);
printf("Function read call count: %d \n",strLen);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char*message){
fput(message,strerr);
fputs('\n',stderr);
exit(1);
}