您的位置:首页 > 理论基础 > 计算机网络

[MFC]Socket基础——以TCP为例

2015-07-19 15:18 543 查看
!!接下来要介绍的Socket函数都是和操作系统无关的,即不管是在Unix、Linux、Mac OS X还是Windows,这些函数都是标准版本的,可以在任意平台上使用,并且都是C语言版的;

1. Socket套接字的概念:

1) 套接字这个名称的背景:

i. 拿最早的电话机来讲,刚有电话的时候如果想拨通另一端的电话,需要电话公司将两个电话所对应的插头插入两个线路互通的插孔中,而套接字就是这种插孔了;

ii. 套接字其实应该叫“套字接”,即理解为“包着字节的接口”,由于计算机互联的协议都是字型协议(即字节、字的那种字),而应用程序只要插进该接口就可以和其它也插入该接口的应用程序进行通信;

iii. Socket的英文翻译就是“建立在一种平台或机器上的插孔,用于连接两个电器设备使之能相互通信”,也就是说Socket可用于任何两个电子设备之间的信息通信,不管使用的是什么协议(HTTP、TCP、UDP等),都要使用Socket通信;

2) 今后,就将Socket称作网络通信接口,也就是说Socket编程实质上就是一种接口编程;

2. 如何确定一个套接字呢?——套接字的寻址方式

1) 和电话网类似,两个人要交流,首先需要知道两个人的地址才行,因此套接字中一定包含通信双方的网络地址,如果使用的是TCP/IP协议,则该地址就是IP地址,如果是其它协议,则地址则会是其它模式;

2) 但计算机的相互通信实质上是两台计算机中的应用程序在进行通信,比如用uto下载文件,那实质上就是两台电脑的uto之间进行信息交换,因此IP只是确定了计算机的地址,但还需要另一个参数来确定参与通信的到底是哪个应用程序,该参数就是端口号,因此套接字里还封装了端口号;

3) 通信双方各持有一个套接字,各套接字封装了各自的网络地址和端口号,双方应用程序只要绑定了各自的套接字就能进一步通信 了;

4) 以上确定套接字的方法就是套接字的寻址方式;

3. 寻址方式的代码实现:

1) 即套接字地址结构,就理解为网络通信接口的地址:

struct sockaddr_in { // 套接字地址
short sin_family; // 指定是哪种地址家族
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // 网络地址,如IP地址等,有sin_family决定
char sin_zero[8]; // 无用,为了兼容第一代Socket版本,全设为0
};
i. sockaddr_in:即Socket Address - internet的缩写,即网络接口地址的意思,注意internet的i是消息而不是大写,即表示普通的网络而不是Internet英特网!

ii. sin_:该前缀就是Socket internet的缩写;

iii. sin_family:即地址家族,是指上隐含指定了使用的是哪种协议,如果是使用基于IP地址的TCP、UDP协议等,就将这项设为AF_INET,即Address Family internet的缩写,即互联网地址家族;

2) in_addr结构:该结构确定了套接字的网络地址,如IP地址等,也可以是其它地址,这取决于sin_family

struct in_addr { // 网络地址
union {
struct { u_char s_b1, s_b2, s_b3, s_b4 } S_un_b; // 4字节版本
struct { u_short s_w1, s_w2 } S_un_w; // 2字版本
u_long S_addr // 1双字版本
} S_un;
};
i. 地址一定是32位的,为了编程需要,有时需要分别对地址中的字节、字等进行操作,避免程序员使用麻烦的位操作,从而利用联合体给出了字节、字、双字的三个版本,可以是编程更加灵活;

ii. in_addr:即internet Address的缩写,即网络地址;

iii. s_和S_前缀就是Struct的缩写;

iv. un_即Union的缩写;

v. b和w分别是byte和word的缩写;

3) 为地址赋值:由于in_addr是二进制的,而我们平时习惯用数字加点的形式来表示地址,比如"218.3.147.5",因此可以使用inet_addr函数将“数字点”形式的字符串转化成相应的二进制网络地址;

i. 函数原型:unsigned long inet_addr(const char FAR* cp);

ii. FAR是一种指针修饰,定义为#define FAR far,用来区别长指针和短指针,原来16位模式下是短指针,现在32位都是长指针了,为了兼容,可以不用理睬;

iii. cp:即character pointer的缩写,即字符串指针,以'\0'结尾,这里就是指“数字点”的字符串;

iv. 如果转化成功则返回二进制的网络地址,否则返回INADDR_NONE;

v. 示例:

unsigned long addr = inet_addr("192.168.3.10");
if (INADDR_NONE == addr) {
printf("Invalid Address!");
}
else {
printf("Valid!");
}


4. 网络字节顺序:

1) 字节顺序即传输的数据的字节的排列顺序,分为小端和大端;

2) 小端:数据的低位在低字节,高位在高字节,比如小端系统中,0xABCD在内存中存储为0xCD, 0xAB,从左往右表示地址上升;

3) 大端:数据的低位在高字节,高位在低字节,比如大端系统中,0xABCD在内存中存储为0xAB, 0xCD,从左往右表示地址上升;

4) 不同计算机平台内部的数据模式有大端也有小端(但是95%以上都是小端的),但是网络中传输数据是不同计算机平台之间的,因此必须统一字节顺序,而网络字节顺序国际统一使用大端,因此计算机发送、接受网络数据之前都需要先调整字节顺序;

5) sockaddr_in结构中所有的内容都是按照网络字节顺序的,所以对该结构进行填充的时候一定要注意:

i. 幸好inet_addr转换函数的返回值本身就是按照网络字节顺序的;

ii. 但是其它数据,如sin_family等就需要注意了,但幸好Windows提供了一系列转换函数来达到这个目的;

iii. 转换函数的名称一般以hton作为前缀,即Host To Net的缩写,即主机顺序转化为网络顺序,或者是ntoh,即Net To Host,即网络顺序转化为主机顺序;

iv. 主要有这几种:

// 短整型之间的互转
u_short htons(u_short hostshort);
u_short ntohs(u_short netshort);

// 长整型之间的互转
u_long htonl(u_long hostlong);
u_long ntohl(u_long netlong);

// 字符串(数字点)和网络地址之间互转
unsigned long inet_addr(const char FAR * cp);
char FAR* inet_ntoa(struct in_addr in); // 失败返回NULL
v. 示例:addr.sin_port = htons(80); // 将端口号80赋给sin_port,但是sockaddr_in的所有一切都是网络字节顺序的;

5. TCP编程的步骤:

!!需要先用WSAStartup来启动Windows Socket API库,即对Socket库初始化;

1) 确定套接字地址(sockaddr_in):就是上面所讲的内容;

!!!服务器端的套接字地址可以是任意的(即INADDR_ANY,表示可以接受任何计算机的请求),而客户端所确定的套接字地址就是其请求连接的目标服务器的地址!!

2) 建立套接字,服务器端使用bind将套接字和自己的套接字地址绑定起来,客户端用connect请求连接服务器;

3) 服务器端使用listen监听客户端的请求,使用accept接受客户端的请求;

4) 双方使用send和recv进行数据数据的发和收;

5) 双方各自调用closesocket关闭套接字(释放内存中的资源);

!!最用后使用WSACleanup来释放Socket库;

6. Winsock库——初始化和释放:

1) 在Windows环境中编写网络通信程序需要使用Windows提供的Windows Sockets库,简称Winsock,之前讲过的所有API都隶属于Winsock库,只不过是国际通用的标准版Socket函数的Windows版实现;

2) Winsock的很多函数的名称以WSA作为前缀,即Windows Sockets API的缩写;

3) 建立具有网络通信功能的Windows工程时,MFC工程必须在向导的第二步开启“Windows Socket”选项,如果忘记开启则在StdAfx.h中添加afxsock.h头文件也行,而对于非MFC工程,则必须添加winsock2.h头文件(不管是控制台程序还是普通的Win32程序);

!其次,所有的Winsock函数都来自WS2_32.DLL动态链接库,可以的是VC并不会在默认情况下链接该库,因此需要手动链接一下,即在Project Settings菜单项中的Link选项卡的Object/library modules中添加ws2_32.dll库名,各个库名之间用空格隔开即可,这个步骤不管是MFC还是其它类型的程序都需要;

!!winsock2.h和ws2_32.dll中的2都表示Winsock版本2;

4) 虽然链接了ws2_32.dll库并不代表可以立即使用Winsock函数了,还需要对该库进行初始化;

i. 使用WSAStartup来初始化Winsock库;

ii. 函数原型:

int WSAStartup(
WORD wVersionRequired, // 指定库的版本号
LPWSADATA lpWSAData // 将指定的库的信息保存到该结构中
);
iii. 由于在不同的平台上编写网络通信程序可能需要不同版本的Winsock库实现,因此第一个参数就是您需要使用的Winsock库版本号;

iv. 该版本号由两个字节表示,低位字节表示主版本号,高位字节表示副版本号,比如版本号2.0中主版本号就是2,副版本号就是0;

v. 可以用MFC提供的函数MAKEWORD来构造要求的版本号:WORD MAKEWORD(BYTE bLow, BYTE bHigh);,这里可以版本号2.0就可表示为MAKEWORD(2, 0);

vi. WSADATA结构中包含了指定版本号的信息,如版本号、系统状态、描述信息等,这里用不到,先不用关心;

vii. 初始化示例:

WSADATA data;
WORD wVersionRequested = MAKEWORD(2, 0);
::WSAStartup(wVersionRequested, &data);
5) 当程序要退出的时候应该释放该Winsock库,使用WSACleanup即可:int WSACleanup();

!!startup和cleanup的返回值都表示调用是否成功,如果成功返回0,否则就返回一个非0的错误代码;

7. 建立和关闭套接字:

1) 前面一直讲的都是套接字地址,即sockaddr_in结构,它只是套接字的寻址方式,但不是套接字本身,表示套接字本身的结构是SOCKET,要想正常使用套接字进行网络通信,必须将SOCKET结构和相应的sockaddr_in地址绑定起来;

!!其实SOCKET是句柄类型,它指向内存中的套接字资源,套接字无法用一个简单的结构体来表示;

2) Winsock中创建套接字资源:使用函数socket即可

i. 原型:SOCKET socket(int AF, int type, int protocol);

ii. AF:还是Address Family的意思,如果是TCP/IP协议就选择AF_INET即可;

iii. type:表示套接字的类型,总共有三种,以SOCK_作为前缀

SOCK_STREAM:流式套接字,基于TCP协议

SOCK_DGRAM:数据包套接字,基于UDP

SOCK_RAW:原始套接字,一般很少用到

iv. 该函数直接返回在内存中创建的套接字资源的句柄;

v. 由于AF已经指定了具体的协议,因此protocol参数实际上没有用,只不过是旧版本遗留的,只要填个0就行了;

3) 创建一个空的套接字:SOCKET s = ::socket(AF_INET, SOCK_STREAM, 0);

!!但是该创建好的套接字还没有确定寻址方式,因此还要进一步和套接字地址进行绑定;

4) 关闭套接字:即使用closesocket函数来释放socket()函数在内存中创建的套接字资源

i. 原型:int closesocket();

ii. 返回值如果为0则表示成功释放,否则返回具体的错误码;

8. 服务器端绑定套接字地址:

1) 使用bind函数将套接字地址绑定进套接字资源中;

2) 函数原型:

int bind( // 成功返回0,否则返回错误码
SOCKET s, // 套接字句柄
const struct sockaddr_in FAR* name, // 套接字地址结构的指针
int namelen // 套接字地址结构体的大小(字节)
);
3) 示例:bind(s, (sockaddr*)&addr, sizeof(sockaddr_in));

!!注意:sockaddr和sockaddr_in之间的区别,sockaddr_in只能用于TCP/IP协议的网络地址,而sockaddr用于任意写一下的网络地址,也就是说sockaddr是sockaddr_in的超集,可以看一下sockaddr结构的定义:

struct sockaddr {
u_short sa_family;
char sa_data[14];
};
!可以看到实际大小和sockaddr_in是一样的,只不过最后的14个字节并没有规定具体的结构,可以根据sa_family指定的协议往sa_data中填相应的数据,只不过TCP/IP太常用,所以单独为其做了一个sockaddr_in结构而已;

!既然bind中第二参数指定为sockaddr结构的指针,所以还是强制转换以下类型的好!!时刻保持类型一致有助于程序的健壮;

!!如果服务器端的Socket地址,即S_addr为INADDR_ANY,就表示服务器端可以接受任意计算机的请求,INADDR_ANY其实就是0.0.0.0地址的宏,是32位二进制的;

9. 客户端请求连接服务器:

1) 使用connect函数请求连接;

2) 函数原型:

int connect(
SOCKET s,
const struct sockaddr FAR *name, // 目标服务器的地址
int namelen
);
!!注意:参数和bind相同,只不过地址是目标服务器的地址

!!返回值意义也一样,也是成功返回0,否则返回错误码;

3) 该函数其实会将空的套接字s和目标服务器地址绑定起来,完成连接后,就可以利用绑定着目标服务器的套接字s来收发数据(交流信息);

10. 监听和接受请求:

1) 在套接字建立好后,服务器进程就要监听客户端进程的请求连接信号,而客户端则需要主动请求连接服务器端进程;

2) 服务器端的listen函数:

i. 原型:int listen(SOCKET s, int backlog);

ii. 同样也是成功返回0,失败返回错误代码;

iii. s就是服务器端的套接字句柄;

iv. backlog:就是积压的意思,即一次可以最多可以监听多少个客户端请求,可以使用宏SOMAXCONN来表示本地平台的监听能力,即Socket Maximum Connections,即最大监听数量,如果实际监听到的数量大于backlog指定的值就会报错(通过错误码返回);

3) 一旦监听到请求,就可以立马调用accept函数接受请求并返回一个绑定着该发出请求的客户端的地址的套接字,然后可以利用该套接字和客户端交流信息(收发数据):

i. 函数原型:

SOCKET accept( // 返回绑定客户端地址的套接字句柄,就是s
SOCKET s, // 服务器的套接字
struct sockaddr FAR* addr, // 空的套接字地址
int FAR* addrlen // 套接字地址结构的大小(指针)
);
ii. 空的套接字地址用于接受发出请求的客户端的地址,而该地址就是通过listen存放在服务器套接字s的缓存中,accept可以将缓存中接收到的这个地址拿出来放到空的addr里面!!!

iii. 非常奇葩的是addrlen是一个int型指针,虽然其指向的值还是sizeof(sockaddr),但这里就是非得用指针,一定要小心!!!

iv. 将返回一个新的绑定着客户端地址的套接字,可以利用该套接字和客户端进行信息交换;

v. 例如:

int sizeAddr = sizeof(sockaddr);
s_client = ::accept(s_sever, (sockaddr*)&addrClient, &sizeAddr);
send(s_client...
...


!!!注意:双方进行通信的时候都是通过持有对方地址的套接字进行通信的!!

!比如,在服务器中:send(s_client...

!在客户端中:send(s_sever...

!!recv也一样,第一个参数都是绑定着对方地址的套接字;

!也就是说数据的收发必须持有对方地址的套接字;

11. 数据收发:

1) 双方都是用send和recv进行数据的发送和接受;

2) 函数原型:

i. send: int send(SOCKET s, const char FAR* buf, int len, int flags);

ii. recv:int recv(SOCKET s, char FAR* buf, int len, int flags);

3) s都是绑定对方地址的套接字;

4) 两个函数的buf和len的意义相同,buf就是交换的数据所在的缓冲区的指针,len就是数据区的大小(字节);

5) flags表示函数调用方式,通常设置为0即可;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: