计网课程推荐哈工大MOOC,节奏快讲得细又条理清晰,好爱!学习笔记之后会相应出,想上手的同学可以直接开始搓以下链接视频P32-P37:
https://www.bilibili.com/video/BV1Up411Z7hC?from=search&seid=14245830410862650079www.bilibili.com此博客为第一章节,主要关于Socket的编程简介和相关API接口函数调用总结基础知识梳理。
Socket API套接字介绍:
我们知道传统网络结构层分为五层:应用层,运输层,网络层,链路层和物理层。Socket即为连接应用层和底层网络传输的黏合剂。
网络应用程序是由通信进程对组成,每对互相通信的应用程序进程互相发送报文,他们之间的通信必须通过下面的网络来进行。为了将应用程序和底层的网络通信协议屏蔽开来,采用套接字(Socket)这样一个抽象概念来作为应用程序和底层网络之间的应用程序编程接口(API)。
Socket是一个抽象概念,代表了通信双方的端点(Endpoint),通信双方通过Socket发送或接收数据。
因为网络应用程序是进程之间的通信,为了唯一的标识通信对等方的通信进程,套接字必须包含2种信息:(1) 通信对等方的网络地址。(2) 通信对等方的进程号,通常叫端口号。
Unix操作系统下实现TCP/IP网络通信协议的开发接口:BSD Sockets
Windows操作系统也提供了一套网络通信协议的开发接口:Windows Sockets或简称Winsock。Winsock 是通过动态链接库的方式提供给软件开发者,而且从Windows 95以后已经被集成到了Windows操作系统中。
Winsock主要经历了2个版本:Winsock 1.1和Winsock 2.0。Winsock 2.0是Winsock 1.1的扩展,它向下完全兼容。
Winsock同时包括了16位和32位的编程接口,16位的Windows Socket 2应用程序使用的动态链接库是WINSOCK.DLL,而32位的Windows Socket应用程序使用WSOCK32.DLL(Winsock 1.1版)和WS2_32.DLL(Winsock 2.0版)。另外,使用Winsock API时要包含头文件winsock.h(Winsock 1.1版)或winsock2.h(Winsock 2.0版)。
Socket 套接字编程原理:
Socket的两种类型:在Winsock里,用数据类型SOCKET作为Windows Sockets对象的句柄,就好像一个窗口的句柄HWND、一个打开的文件的文件指针一样。下面我们会看到,在Winsock API的许多函数里,都会用到SOCKET类型的参数。
1.流类型(Stream Sockets):流式套接字提供了一种可靠的、面向连接的数据传输方法,使用传输控制协议TCP。
2.数据报类型(Datagram Sockets):数据报套接字提供了一种不可靠的、非连接的数据包传输方式,使用用户数据报协议UDP。
SocketI/O的两种模式:一个SOCKET句柄可以看成代表了一个I/O设备。在Windows Sockets里,有2种I/O模式:
1.阻塞式I/O(blocking I/O):在阻塞方式下,收发数据的函数在调用后一直要到传送完毕或者出错才能完成,在阻塞期间,除了等待网络操作的完成不能进行任何操作。阻塞式I/O是一个Winsock API函数的缺省行为。
2.非阻塞式I/O(non-blocking I/O):对于非阻塞方式,Winsock API函数被调用后立即返回;当网络操作完成后,由Winsock给应用程序发送消息(Socket Notifications)通知操作完成,这时应用程序可以根据发送的消息中的参数对消息做出响应。Winsock提供了2种异步接受数据的方法:一种方法是使用BSD类型的函数select(),另外一种方法是使用Winsock提供的专用函数WSAAsyncSelect()。
使用数据报套接字:
首先,客户机和服务器都要创建一个数据报套接字。接着,服务器调用bind()函数给套接字分配一个公认的端口。一旦服务器将公认的端口分配给了套接字,客户机和服务器都能使用sendto()和revfron()来传递数据报。通信完毕调用closesocket()来关闭套接字。流程如图所示:
使用流式套接字:
由于流式套接字使用的是基于连接的协议,所以你必须首先建立连接,而后才能从数据流中读出数据,而不是从一个数据报或一个记录中读出数据,其流程函数调用如图所示。
套接字部分库函数:
详细讲解将在下篇给出,在此处只做列表和重点标注:
重点Socket API函数:[按调用顺序]
WSAStartup();WSACleanup();socket();bind()通常客户端不需;closesocket():释放关闭套接字;Listen():TCP套接字监听模式;Accept():仅用于服务器端TCP套接字,接收链接请求,创建新的套接字;Recv:接收数据(TCP套接字或连接模式的客户端UDP套接字;Setsockopt():设置套接字选项参数;Getsockopt:获取套接字选项参数.
服务器端的流套接字处于被动监听状态 int listen(sd,queuesize):仅用于服务器端,仅用于面向对象的流套接字
Connect(sd,saddr,saddrlen):仅用于客户端,但TCP(建立TCP连接)和UDP(指定服务器端点地址)都可用,使客户套接字与特定计算机的特定端口的套接字(服务器的套接字)进行连接
Accept:Newsock=accept(sd,caddr,caddrlen):只用于服务器端函数,仅用于TCP套接字,服务器调用accept函数从处于监听状态的流套接字sd的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道
Send(sd,*buf,len,flags):TCP套接字或调用了connect的UDP客户端套接字Sendto(sd,*buf,len,flags,destaddr,addrlen);UDP服务器或未调用connect函数的UDP客户端套接字
Recv(sd,*buffer,len,flags);从TCP连接的另一端接受数据,或者从调用了connect函数的UDP客户端套接字接收服务器发来的数据。Recvfrom(sd,*buf,len,flags,senderaddr,saddrlen);从UDP服务器端套接字与未调用connect函数的UDP客户端套接字接收对端数据。
关于网络字节顺序:
TCP/IP定义了标准的用于协议头中的二进制整数表示:网络字节顺序(network byte order)
可以实现本地字节顺序与网络字节顺序间转换的函数:
Htons:本地字节顺序--->网络字节顺序(16bits)
Ntohs:网络字节顺序--->本地字节顺序(16bits)
Htonl:本地字节顺序--->网络字节顺序(32bits)
Ntohl:网络字节顺序--->本地字节顺序(32bits)
解析服务器:
1.客户端可能使用域名(eg:http://study.163.com)或ip地址(eg:123.58.180.121)标识服务器:ip地址需要32位二进制IP地址;所以我们调用函数inet_addr()实现点分十进制ip地址到32位IP地址的转换,gethostbyname()「返回hostent指针」,实现域名到32位IP地址的转换
2.客户端还可能使用服务名(如HTTP)标识服务器端口;需要将服务名转换为熟知端口号,函数getservbyname()「返回一个serven结构指针t」
解析协议号:
客户端可能使用协议名(如TCP)指定协议,我们需要将其解析或映射为它的协议号,函数getprotobyname()实现协议名到协议号的转换,返回一个指向结构protoent的指针.
TCP客户端软件流程:
1.确定服务器IP地址与端口号
2.创建套接字
3.分配本地端点地址(IP地址+端口号)//系统自动完成
4.连接服务器(套接字)
5.遵循应用层协议进行通信
6.关闭或释放连接
UDP客户端软件流程:
1.确定服务器IP地址与端口号
2.创建套接字
3.分配本地端点地址(IP地址+端口号)//系统自动完成
4.指定服务器端点地址,构造UDP数据报
5.遵循应用层协议进行通信,UDP先发
6.关闭或释放套接字
Windows Socket 2的扩展特性
Winsock 2.0简介:
Winsock 1.1原先设计的时候把API限定在TCP/IP的范畴里,它不象Berkerly模型那样支持多种协议。而Winsock 2.0正规化了一些其它的协议(如ATM、IPX/SPX和DECNet协议)的API。
Winsock 2.0之所以能支持多种协议,是因为Winsock 2.0在Windows Sockets DLL和底层协议栈之间定义了一个SPI(Service Provider Interface)接口,这样,通过一个Windows Sockets DLL可以同时访问底层不同厂商的协议栈。
Winsock 2.0不仅允许多种协议栈的并存,而且从理论上讲,它还允许创造一个与网络协议无关的应用程序。Winsock 2.0可以基于服务的需要透明地选择协议,应用程序可以适用于不同的网络名和网络地址。
Winsock 2.0还扩展了它的API函数集,当然Winsock 2.0是向下兼容的,可以把Winsock 1.1的代码原封不动地用在Winsock 2.0中。
Winsock 2.0新特性:
下面列出了一些Winsock 2.0的重要新特性:
- 多重协议支持:SPI接口使得新的协议可以被支持。
- 传输协议独立:根据服务提供不同的协议。
- 多重命名空间:根据需要的服务和解析的主机名选择协议。
- 分散和聚集: 从多个缓冲区接受和发送数据。
- 重叠I/O和事件对象:增强吞吐量。
- 服务质量(Qos):协商和跟踪网络带宽。
- 条件接受:可以选择性地决定是否接受连接。
- Socket共享:多个进程可以共享一个SOKCKET句柄。
MFC Socket编程
MFC类中有二个类能支持Socket编程:CAsynSocket类和CSocket类。
CAsynSocket:
CAsynSocket类封装了Windows Socket的API,因此我们可以用面向对象的方法调用Sokcet。这个类中,必须自己处理阻塞和Unicode与其他字节集的字节顺序转换,因此CAsynSocket类对Windows Socket的API的封装是在教低层次上进行。
从该类的名字可以就看出,CAsynSocket类提供了异步通信编程模式:在默认状态下由该类创建的套接字是非阻塞的套接字,在这个非阻塞套接字上进行的包括接受数据和建立连接等在内所有操作也都是非阻塞的。对于异步通信编程(非阻塞式Socket),该类将网络事件加入到Windows的消息循环机制当中,使得用户能像响应普通的键盘、鼠标、窗口重画等事件一样来响应网络事件,如收到连接请求、有数据到来等。
由于在CAsynSocket类是由OnAccept()、OnReceive()等虚函数来响应各种网络事件的,因此需要在编程时对这些虚函数进行重载,以完成具体的功能。这就需要从CAsynSocket类派生一个继承类,并在该派生类中进行程序设计。
CSocket:
CSocket继承于CAsynSocket,是Windows Socket API的高层抽象。CSocket通常和CSocketFile和CArchive类混合使用,这二个类负责数据的发送和接收。
除了继承CAsynSocket的函数成员外,CSocket最重要的特性是通过CArchive、CSocketFile来发送接收数据,从而将编程人员和Socket的繁琐细节屏蔽开来。你需要做的只是创建Socket、CArchive对象、CSocketFile对象并将它们关联起来,然后你只是从CArchive对象中读取数据或向CArchive对象写数据。三个类之间的关系如图.
CArchive类:
CArchive类允许你将一个内存中的对象序列化到永久存储的介质中去。当要创建一个CArchive对象时,必须先创建一个CSocketFile对象并将其关联到CArchive对象(事先创建好的CSocketFile对象的指针作为CArchive的构造函数的第一个参数)。
CSocketFile类:
CSocketFile类从CFile类继承而来,因此它的成员函数请参考MSDN中CFile类的文档。这里仅介绍如何将一个Socket和一个CSocketFile对象关联起来,这通过将一个CSocket对象的指针传递给CSocketFile的构造函数来完成。CSocketFile的构造函数的详细说明请参考MSDN文档。
CSocket的编程模型:
创建和使用CSocket的前面步骤和CAsynSocket完全一样(因为CSocket就是从CAsynSocket继承而来)。区别就在当创建好CSocket对象后,你要将CSocket对象和CSocketFile对象关联起来,然后再将CSocketFile对象和CArchive对象关联起来。
这里必须要特别注意的是,如果你创建的是一个数据报CSocket对象,则CSocketFile对象不能再和CArchive对象关联了。换句话说,只有和流式CSocket对象关联的CSocketFile对象才能进一步和CArchive对象关联。
一旦关联成功,我们只需要从CSocketFile对象或CArchive对象输入或输出数据而不用去考虑Socket接收或发送数据的细节。这个时候,我们从网络输入数据或从向网络上发送数据和我们对一个普通外设(如文件)进行输入输出操作所使用的函数完全是一样的。
下面一段示例代码说明了如何把CSocket类和CArchive、CSocketFile类关联起来:
(1)将通过socket发送和接受的消息封装在一个可序列化的类中。
//CMessg类用来封装通过socket发送和接受的消息
//通过从CObject类继承来的序列化机制来实现
// CMessg的声明:CMessg.h
class CMessg : public CObject
{
protected:
DECLARE_DYNCREATE(CMessg) //序列化机制所需要的宏
public:
CMessg();
// Attributes
public:
CString m_strText; //发送或接受的消息
// Operations
public:
void Init(); //初始化函数
// Implementation
public:
virtual ~CMessg();
//重载该函数以实现序列化IO
virtual void Serialize(CArchive& ar);
#ifdef _DEBUG
virtual void AssertValid() const; //调试用函数
virtual void Dump(CDumpContext& dc) const; //调试用函数
#endif
};
//CMessg实现:CMessg.cpp
#include "CMessg.h"
IMPLEMENT_DYNCREATE(CMessg, CObject)//必须的宏
CMessg::CMessg() { Init();}
CMessg::~CMessg(){}
void CMessg::Init(){ m_strText = _T("");}
// CMsg serialization
void CMessg::Serialize(CArchive& ar)
{
if (ar.IsStoring()){ ar << m_strText; }
else { ar >> m_strText;}
}
// 调试用
#ifdef _DEBUG
void CMessg::AssertValid() const
{
CObject::AssertValid();
}
void CMessg::Dump(CDumpContext& dc) const
{
CObject::Dump(dc);
}
#endif //_DEBUG
MFC中的多线程:
在MFC中线程分为二种:工作者线程(Worker thread)和用户界面线程(UI thread)。二者的区别在于:工作者线程是没有消息队列和消息循环机制的,而用户界面线程则有自己的消息队列和消息循环,因此可以在用户界面线程里和主线程一样处理各种消息如键盘、鼠标消息等。
通常的做法是在主线程里处理用户界面产生的各种消息,在工作者线程里处理各种耗时的、或阻塞式的IO动作,比如从Socket中接受和发送数据(如阻塞式的send()调用或recv()调用)等。
这里要注意的是:MSDN建议最好在辅助工作者线程里使用阻塞式Socket,因为如果在工作者线程里使用异步(非阻塞式)Socket,则意味着你必须要在工作者线程里等待事件通知消息,而工作者线程是没有任何消息机制的。因此,在这里建议如果要使用多线程,则在辅助工作者线程不要使用CAsynSocket类,而使用阻塞式Socket。不幸的是,微软没有提供阻塞式Socket类,因此在这种情况下最好自己将Winsock API函数封装成阻塞式Socket类。在《Visual C++ 6.0技术内幕》里有完整的自己封装的阻塞式Socket类CBlockingSocket的代码示例以及如何利用该类实现多线程的Web服务器的代码示例。
MFC提供了全局函数AfxBeginThread来创建工作者线程,该函数原型如下:
CWinThread * AfxBeginThread(
AFX_THREADPROC pfnThreadProc
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize=0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);