1.2 套接字及其种类
我们每天都在使用QQ,但大家想过像QQ这样的网络通信软件是如何开发出来的吗?其实,任何网络通信软件的实现都离不开一种关键技术,那就是套接字(socket)。套接字实际上是TCP/IP网络编程的编程接口。
1.2.1 什么是套接字
为了让开发者能够方便地开发网络应用软件,1983年,由加州大学伯克利分校(Berkeley)在Unix上推出了一种应用程序访问通信协议的操作系统调用Socket(套接字)。Socket的出现,使程序员可以很方便地访问TCP/IP协议,从而开发出各种类型的网络应用程序。
随着Unix的应用推广,套接字在编写网络软件中得到了极大的普及。1991年,微软将Socket套接字引入到Windows操作系统中,称为WinSock,成为在Windows系统下开发网络应用程序非常快捷有效的工具。
套接字存在于通信区域中。通信区域也叫地址族,它是一个抽象的概念,主要用于将使用套接字通信的进程的共有特性综合在一起。套接字通常只与同一区域的套接字交换数据(也有可能跨区域通信,但这只在执行了某种转换进程后才能实现)。Windows Sockets只支持一个通信区域:网际域( AF_INET),这个域能够被使用网际协议簇通信的进程使用。
1. TCP/IP协议与Socket的关系
TCP/IP只是一个协议栈,用来定义网络的运行机制,如果要用程序来具体实现TCP/IP协议的功能,就必须使用TCP/IP协议对外提供的一个编程接口,而Socket就是TCP/IP协议提供的编程接口,就像Win32 API是Windows提供的编程接口一样。
TCP/IP协议族包括应用层、传输层、网络层、接口层,而Socket是应用层与TCP/IP协议族通信的中间软件抽象层。Socket编程接口的位置如图1-5所示。
图1-5 Socket在TCP/IP协议族中的位置
2. 标识通信进程的方法
在网络通信中,通信的各方进程如何标识自己呢?众所周知,一个IP地址可以标识一台主机,而“IP地址:端口号”可标识主机上的一个进程。TCP协议和UDP协议的端口是不共用的,因此端口又分为TCP端口和UDP端口,也就是说端口号必须指定协议名。
(1)半相关
网络中用一个如下的三元组可以在全局中唯一标志一个进程,这样一个三元组,叫做一个半相关(half-association),它指定了连接的每半部分。
(协议,本地地址,本地端口号)
(2)全相关
一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要如下一个五元组来标识。
(协议,本地地址,本地端口号,远地地址,远地端口号)
这样一个五元组,叫做一个全相关(association),即两个协议相同的半相关才能组合成一个全相关。
3. 套接字地址的结构
在Socket网络通信中,需要使用struct sockaddr和struct sockaddr_in两种结构体来处理套接字通信的地址。简而言之,sockaddr表示WinSock的通用地址,它把IP地址和端口号混在一起存储,该地址设计之初就是为了兼容不同网络协议族的地址,而sockaddr_in 表示Internet环境下的地址,该结构体解决了sockaddr的缺陷,把IP地址和端口号分别存储在两个变量中。
sockaddr的地址结构如下:
struct sockaddr {
u_short sa_family; // 2位的地址协议族
char sa_data[14]; }; // 14位的协议地址
sockaddr_in的地址结构如下:
struct sockaddr_in{
short sin_family; /*AF_INET*/
u_short sin_port; /*16位端口号,网络字节顺序*/
struct in_addr sin_addr; /*32位IP地址,网络字节顺序*/
char sin_zero[8]; }; /*保留*/
因此,在为套接字绑定IP地址和端口时需要使用sockaddr_in地址,而sockaddr地址常作为bind、connect、recv、send等函数的参数,表示一种通用的套接字地址。sockaddr和sockaddr_in两种结构体的长度是一样的,都是16个字节,即占用的内存大小是一致的,因此两者可以互相转化。具体方法是,首先定义一个sockaddr_in结构类型的指针变量p,通过强制类型转换让p指向sockaddr结构类型的变量,然后指针p可按sockaddr_in类型对各字段完成赋值。代码如下:
struct sockaddr a;
struct sockaddr_in *p;
p=(sockaddr_in *) &a; /*强制类型转换,其中&a表示这是一个地址,(sockaddr_in *)表示转换成另一种类型的指针变量*/
p->sin_family=AF_INET;
p->sin_port=5566;
p->sin_addr=inet_addr("192.168.1.5");
1.2.2 套接字的类型
为了满足不同种类的通信程序对通信质量和性能的需求,Socket API提供了下列3种类型的套接字。
1)流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。TCP协议即使用流式套接字。
2)数据报套接字(SOCK_DGRAM)
提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。UDP协议即使用数据报式套接字。
3)原始套接字(SOCK_RAW)
该套接字允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。如ping命令、抓包软件都可以用原始套接字来实现。
套接字在编程时对于开发人员是可见的,网络应用程序一般使用同一类型的套接字进行通信。
1.2.3 网络字节顺序
现代计算机具有几种不同的体系结构,如IBM X86,Power PC、苹果 PC等,不同体系结构的计算机在存储多字节数据时存在大端字节顺序和小端字节顺序两种方式,当不同字节顺序的计算机通过网络交换数据时,如果不作任何处理,将会出现严重的问题。例如,一台使用PowerPC系列的CPU,运行Linux系统的主机发送一个16位的数据0x1234到一台采用Intel 酷睿i5系列CPU运行Windows 7的PC时,这个16位的数据将被Intel的CPU解释成0x3412,也就是将整数4660当成了13330。
为了解决这一问题,在编写网络程序时,规定发送端发送的多字节数据必须先转换成与具体主机无关的网络字节顺序,接收端收到数据后,必须将数据再转换回主机字节顺序,网络字节顺序采用的是大端存储的方式。
在WinSock编程中,绑定套接字的IP地址及端口号时必须采用网络字节顺序,而由套接字函数返回的IP地址和端口号在本机进行处理时,则需要转换回主机字节顺序,网络字节顺序和主机字节顺序的转换主要依靠下列几个函数来完成。
1. 端口号转换函数
1)htons()函数
该函数将一个16位的无符号短整型数据由主机字节序转换成网络字节序。由于TCP(或UDP)端口号是一个16位的无符号整型数,范围是从0~65535,因此一般使用htons()函数将主机字节序转换成网络字节序。例如:
addrSer.sin_port=htons(5566); //设置套接字地址的端口号为5566
2)ntohs()函数
该函数将一个16位的无符号整型数据由网络字节序转换成主机字节序,其功能与htons()函数相反。
另外,还有htonl()和ntohl(),这两个函数用于对一个32位的无符号长整型数据进行网络字节顺序和主机字节顺序的相互转换。
2. IP地址转换函数
为了便于书写和记忆,IP地址一般用点分十进制表示。但这并不是计算机内部的IP地址表示方式,计算机内部的IP地址是以无符号长整型方式存储的。为了对用户输入的点分十进制形式的IP地址与程序内部使用的无符号长整型IP地址相互转换,WinSock提供了下面两个函数。
1)inet_addr()函数
该函数将点分十进制字符串表示的IP地址转换成32位的无符号长整型数。函数原型为:
unsigned long inet_addr(const char * cp);
例如,设置套接字的IP地址通常使用下面的语句。
addrSer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); //设置套接字的ip地址
2)inet_ntoa()函数
该函数将一个包含在in_addr结构变量中的长整型IP地址转换成点分十进制形式,其功能与inet_addr()函数正好相反。函数原型为:
char * inet_ntoa(struct in_addr in);
其中,in是一个保存有32为二进制IP地址的in_addr结构变量。
inet_ntoa (fromaddr.sin_addr); //将IP地址从网络字节序转换为主机字节序
1.3 VC编程基础知识
1.3.1 VC字符串处理函数
在网络编程中,经常需要对字符串进行处理。VC中声明字符串有如下几种方式:
char user[10]; //声明长度为10字节的字符串
char user[10]= "小猫叫" //声明固定长度的字符串并赋值,占10字节空间
char user[]= "小猫叫" //声明字符串并赋值,占7个字节空间
const char * wel="欢迎您,尊敬的" //声明字符串常量,该字符串不能修改
char* Buf= new char[len]; //字符串长度为变量len,可导致内存溢出
字符串处理函数主要有如下几个,在使用“str”开头的字符串处理函数之前,必须先包含头文件string.h,而sprintf需要头文件stdio.h。
- strcat:用于连接两个字符串,并将连接后的字符串保存到第一个参数代表的字符串中。
- strcmp:比较字符串,常用于判断字符串是否为某个值
- strlen:获取字符串的长度,不包括字符串末尾的’\0’。
- strcpy:复制字符串,常用于给字符串重新赋值。
- sprintf:用于将多个字符串变量或其他变量连接在一起组成一个新的字符串。
- sizeof:这是个运算符,不是函数,它可返回某个变量占用的内存空间。
- memset:用于将某个变量的内存空间清0,语法为:void *memset(void *s,int c,size_t n),总的作用是将已开辟内存空间 s 的首 n 个字节的值设为值 c。
下面利用字符串处理函数制作了一个简单的用户登录程序,制作步骤如下:
在VC6中新建工程,执行菜单命令“文件→新建→工程”,选择“Win32 Console Application”,输入工程名,在“下一步”中选择“一个简单的程序”。输入如下代码:
#include "stdafx.h"
#include "iostream.h" //cin、cout函数的支持文件
#include "stdio.h" //sprintf函数的支持文件
#include "string.h" //字符串处理函数的支持文件
int main(){
char user[10],pwd[10],all[98];
const char * wel="欢迎您,尊敬的",local[]="湖南";
while(1){ //用户输入错误后还有机会继续输入
cout<<"请输入您的姓名\n";
cin>>user;
cout<<"请输入您的密码\n";
cin>>pwd;
cout<< sizeof(pwd)<<" "<<strlen(pwd) <<"\n";
if(strlen(pwd)>=6){
if(strcmp(pwd,"111111")==0){
//strcat(wel,user); //用strcat()连接两个字符串
sprintf(all,"%s%s,您来自%s", wel,user,local); //用sprintf连接3个字符串
cout<< all<<"\n";
break;}
else cout<<"非法用户,拒绝登录\n"; }
else cout<<"密码不小于6个字符\n";
}
return 0; }
提示:main函数的return语句用来退出函数,return 0表示函数正常结束,非0表示函数异常结束。因此,每个main函数中只有1条return语句会被执行。
如果要在VS2010中新建项目,则执行菜单命令“文件→新建→项目”,在图1-6所示的新建项目对话框中,找到Visual C++,选择“Win32控制台应用程序”,在下方“名称”后输入项目名,在“下一步”中单击“完成”即可。
图1-6 新建项目对话框
另外,在VS 2010中,#include "iostream.h"必须写成以下两条语句:
#include "iostream"
using namespace std; //使用命名空间std
1.3.2 VC中新增的数据类型
在使用VC编写程序时,除了可以使用标准C++中的数据类型外,还可使用Visual C++自己定义的一些特有的数据类型,这些数据类型大都基于标准C++中的数据类型重新定义而来。在WinSock编程中经常会要使用,表1-1列出了VC++中常用的数据类型及对应的标准C++中的数据类型。
表1-1 VC++中常用的数据类型及说明
数据类型 | 对应的基本数据类型 | 说明 |
BSTR | unsigned short* | 16位字符指针 |
BYTE | unsigned char | 8位无符号整数 |
DWORD | unsigned long | 32位无符号整数,段地址和相关的偏移地址 |
LONG | long | 32位带符号整数 |
LPARAM | unsigned int | 作为参数传递给窗口过程或回调函数的32位值 |
LPCSTR | const char* | 指向字符串常量的32位指针 |
LPSTR | char* | 指向字符串的32位指针 |
LPVOID | void* | 指向未定义的类型的32位指针 |
UNIT | unsigned int | 32位无符号整数 |
WORD | unsigned short | 16位无符号整数 |
WPARAM | unsigned int | 作为参数传递给窗口过程或回调函数的32位值 |
可见,VC++中的数据类型都是以大写字符出现的,这主要是为了与标准C++的基本数据类型相区别。
VC++中的数据类型的命名也是有规律的,例如:指针类型的命令方式一般是在其指向的数据类型前加“LP”,比如指向DWORD的指针类型为“LPDWORD”;无符号类型一般是以“U”开头,比如“INT”是符号类型,“UINT”是无符号类型。
VC++还提供一些宏来处理基本数据类型:例如:LOWORD和HIWORD这两个宏分别用来获取32位数值中的低16位和高16位字;LOBYTE和HIBYTE用来获取16位数值中的低位和高位字节;MAKEWORD则是将两个字节型数据合成一个16位的WORD类型。