Windows网络编程,相信好多人都知道,但是我们一般都是用其他语言编写,例如C,C++,JAVA,python等等,这些语言都可以,但是汇编语言比较底层,利用它,我们可以更清晰的了解到网络编程的内在部分,这是其他语言不能相比的,好了,废话不多说,这其实就是这次的目的(毕竟水平欠缺,还是先来按照罗云斌老师的WIN32汇编书上的例子加以学习,举一反三吧)。
说道网络编程,现在我所接触到的程序开发,工具软件的使用,库等等都是基于Windows平台的,想要了解Windows的网络编程就必须要知道WinSock接口(也就是在OSI模型的传输层和网络层,说到这里,相信大家都对计算机网络原理的OSI模型比较熟悉吧,这是网络编程的基础,对于IP地址,子网掩码,网关,协议等等,这些都是搞计算机必须清楚的概念吧,在这里省略数千字),windows编程都有一个特点(大家都知道,他隐藏的比较深,只给我们这些程序员提供函数接口,至于函数内部长得什么样子,是丑还是漂亮,跟我们没有关系,他也不让我们知道,就像GDI
接口,所有图像的处理都靠的是函数,我们需要传递的只有参数而已),关于windows网络编程的接口就是Winsock ,那剩下的就是关于这个接口提供的一大堆的函数,下面看一下一张简单的介绍图
上面就是WinSock接口的DLL库文件版本,这些库文件在编写程序的时候,都需要声明相应的头文件,以及自己手动使用函数装入相应的库文件。
接下来就是有关套接字(socket)的内容,套接字就是实现通信的对象,用来作为操作对象(一般对于函数来说),它是建立连接双方对象,就像是打电话的两个人一样。
实现一个简单的TCP服务器端的大致步骤:
1. 创建套接字(socket)
2.绑定IP地址和端口(bind)
3.监听进入的连接(Listen)
4.接受连接(accept)
5.收发数据(send/recv)
6.关闭连接(closesocket)
下面我们就着手程序的实现,首先看一下程序步骤:
资源文件
对话框(模态) | IDD_DIALOG1 |
图标 | IDI_ICON2 |
静态文本框1 | 不用对其进行操作,没有设置ID |
静态文本框2 | IDC_COUNT 用来显示连接的客户端数量 |
程序入口,创建模态对话框 | DialogBoxParam() |
窗口处理过程 | _ProcDlgMain |
监听线程 | _ListenThread |
通信线程 | _ServiceThread |
下面首先来看一下资源文件,这次资源编写依然是使用所见即所得的资源编辑工具ResEdit,上面的介绍中资源文件一共包括四个部分,首先是一个对话框,然后在该对话框中添加图标,和两个静态文本框一个用来显示提示字符,一个用来显示连接服务端的的客户数量,在这里都是使用的该工具默认的ID值,可以在使用该工具创建的工程的文件夹下的resource.h 文件中查看具体的ID值,如果想自己中自定义的话,可以自己添加,关于资源文件不多说了,下面直接看一下资源文件的代码:
以前都是直接复制的代码,但是看着总感觉不够直观,这次直接将截图上传过来,感觉清楚多了。
下面来看一下程序的实现过程,在这里首先需要注意的一个问题就是,看如下代码:
注意:
push
ecx
invoke
CreateThread,NULL,0,offset _ServiceThread,eax,NULL,esp
pop
ecx
看到这里,首先需要温习一下函数参数的压栈和调用函数时参数的使用,invoke伪指令就是先检查函数的参数
然后将参数压栈(push ),在这里CreateThread函数的最后一个参数lpThread (就是保存的新线程的ID)并没有什么用处,不需要专门为它定义一个变量,这里使用push ecx指令临时在堆栈中分配一个dword空间供其使用,调用时使用esp将该临时空间的地址传给函数即可,然后在函数返回的时候直接用一个pop ecx操作释放堆栈空间,当时我还在想,为什么只把ecx释放呢?因为其它的都是函数中的参数,函数内会将使用的堆栈空间释放掉,这个我们不用操心,为了更好地理解,我把该程序使用OD载入,我们来看一下,这几句代码是怎么操作的,如下图:
首先push ecx 在堆栈中开辟一个空间,而此时栈顶指针esp(栈底指针是ebp)正好指向 ecx(也就是开辟的空间),经典之处就在这里,当函数调用的时候,esp作为CreateThread函数的最后一个参数处理,此时esp是指向开辟的空间的,所以该空间存储的就是函数的最后一个参数的返回值(线程ID),因为这个值没有什么用处,所以不直接指定变量浪费空间。
其它的就不多说了,下面来看一下程序源代码:
下面来看一下一些陌生的结构类型和API函数:
Select()
功能:
确定一个或多个套接口的状态,本函数用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息,用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。
原型:
int PASCAL FAR select( int nfds, fd_set FAR* readfds, fd_set FAR* writefds, fd_set FAR* exceptfds, const
struct timeval FAR* timeout);
参数:
nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
readfds:(可选)指针,指向一个fd_set 结构
writefds:(可选)指针,指向一个fd_set结构
exceptfds:(可选)指针,指向一个fd_set结构
timeout:等待时间,指向一个timeval结构
返回值:
elect()调用返回处于就绪状态并且已经包含在fd_set结构中的描述字总数;如果超时则返回0;否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError获取相应错误代码。
当返回位-1时,所有描述符集清0。
当返回为0时,超时不修改任何描述符集。
当返回为非0时,在3个描述符集里,依旧是1的位就是准备好的描述符。这也就是为什么,每次用select后都要用FD_ISSET的原因。
Recv()
功能:
用于已连接的数据报或流式套接口进行数据的接收。
原型:
int recv( _In_ SOCKET s, _Out_ char *buf lpbuf, _In_ int len, _In_ int flags);
参数:
s: 指定读取的套接字句柄
lpbuf: 指向一个用来返回数据的缓冲区
len:指定缓冲区的大小
flags:指定读取时的选项,它可以是MSG-PEEK和MSG_OOB的组合,MSG_PEEK表示返回数据后并不从缓冲区中清除数据
返回值:
若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAENOTCONN:套接口未连接。
WSAEINTR:阻塞进程被WSACancelBlockingCall()取消。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAENOTSOCK:描述字不是一个套接口。
WSAEOPNOTSUPP:指定了MSG_OOB,但套接口不是SOCK_STREAM类型的。
WSAESHUTDOWN:套接口已被关闭。当一个套接口以0或2的how参数调用shutdown()关闭后,无法再用recv()接收数据。
WSAEWOULDBLOCK:套接口标识为非阻塞模式,但接收操作会产生阻塞。
WSAEMSGSIZE:数据报太大无法全部装入缓冲区,故被剪切。
WSAEINVAL:套接口未用bind()进行捆绑。
WSAECONNABORTED:由于超时或其他原因,虚电路失效。
WSAECONNRESET:远端强制中止了虚电路。
linux版本:
第四个参数:
MSG_DONTROUTE 绕过路由表查找。
MSG_DONTWAIT 仅本操作非阻塞。
MSG_OOB 发送或接收带外数据。
MSG_PEEK 窥看外来消息。
MSG_WAITALL 等待所有数据。
返回值:
若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。如果发生错误,返回-1,应用程序可通过perror()获取相应错误信息。
Send()
功能:
经套接字传送消息
原型:
send (int s,*buf lpbuf,size_t len,int flags);
参数:
s:指定套接字句柄
lpbuf:指向要发送数据的缓冲区
Len:指定要发送数据的长度
flags:参数指定选项,一般为0
返回值:成功则返回实际传送出去的字符数,失败返回-1
socket()
功能:
创建一个能够进行网络通信的套接字。
原型:
int socket(int af, int
type, int protocol);
参数:
af:指定套接字使用的地址格式,对于TCP/IP协议族,该参数置AF_INET;
type:
指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM、原始套接字SOCK_RAW(WinSock接口并不适用某种特定的协议去封装它,而是由程序自行处理数据包以及协议首部);
protocol:应用程序所使用的通信协议。此参数可以指定单个协议系列中的不同传输协议。在Internet通讯域中,此参数一般取值为0,系统会根据套接字的类型决定应使用的传输层协议。
返回值:
如果调用成功就返回新创建的套接字的描述符(句柄),如果失败就返回INVALID_SOCKET
CloseSocket()
功能:
关闭一个套接口。更确切地说,它释放套接口描述字s,以后对s的访问均以WSAENOTSOCK错误返回。若本次为对套接口的最后一次访问,则相应的名字信息及数据队列都将被释放
原型:
int PASCAL FAR closesocket( SOCKET s);
参数:
s:一个套接口的描述字(句柄)
返回值:
如无错误发生,则closesocket()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAENOTSOCK:描述字不是一个套接口。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAEINTR:通过一个WSACancelBlockingCall()来取消一个(阻塞的)调用。
WSAEWOULDBLOCK:该套接口设置为非阻塞方式且SO_LINGER设置为非零超时间隔。
htons()
功能:
将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为:高位字节存放在内存的低地址处。
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用big-endian排序方式。
原型:htons{u_short
hostshort}
参数:
hostshort:16位无符号整数
返回值:
TCP/IP网络字节顺序.(32位的IP地址形式)
Bind()
功能:
将一本地地址与一套接口捆绑。
原型参数:
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR* name,
int namelen);
s:标识一未捆绑套接口的描述字。
name:赋予套接口的地址。sockaddr结构定义如下:
struct sockaddr{
u_short sa_family;
char sa_data[14];
};
namelen:name名字的长度。
返回值:
成功返回0,失败返回-1.
Listen()
功能:
监听一个套接字
原型:
int PASCAL FAR listen (SOCKET s, int backlog);
参数:
s: 服务端套接字
backlog:等待连接队列的最大长度,
比方说,你将backlog定为10, 当有15个连接请求的时候,前面10个连接请求就被放置在请求队列中,后面5个请求被拒绝。千千万万要注意:这个10并不是表示客户端最大的连接数为10, 实际上可以有很多很多的客户端(实践证明也是如此)。
返回值:
成功返回0, 失败返回-1.
accept()
功能:
在一个套接口接受一个连接
原型参数:
SOCKET PASCAL FAR accept( SOCKET s, struct sockaddr FAR* addr,
int FAR* addrlen);
s:套接口描述字,该套接口在listen()后监听连接。
addr:(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针,指向存有addr地址长度的整形数。
返回值:
如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。
addrlen所指的整形数初始时包含addr所指地址空间的大小,在返回时它包含实际返回地址的字节长度。
错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAEFAULT:addrlen参数太小(小于socket结构的大小)。
WSAEINTR:通过一个WSACancelBlockingCall()来取消一个(阻塞的)调用。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAEINVAL:在accept()前未激活listen()。
WSAEMFILE:调用accept()时队列为空,无可用的描述字。
WSAENOBUFS:无可用缓冲区空间。
WSAENOTSOCK:描述字不是一个套接口。
WSAEOPNOTSUPP:该套接口类型不支持面向连接服务。
WSAEWOULDBLOCK:该套接口为非阻塞方式且无连接可供接受。
下面来看一下几个陌生的结构:
INADDR_ANY:
INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或"所有地址"、"任意地址"。
一般来说,在各个系统中均定义成为0值。
WSAData
struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYSSTATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char *lpVendorInfo;
};
元素介绍:
wVersion
Windows Sockets DLL期望调用者使用的Windows Sockets规范的版本。 高位字节存储副版本号, 低位字节存储主版本号,可以用WORD MAKEWORD(BYTE,BYTE ) 返回这个值,例如:MAKEWORD(1,1)
wHighVersion
这个DLL能够支持的Windows Sockets规范的最高版本。通常它与wVersion相同。
szDescription
以null结尾的ASCII字符串,Windows Sockets DLL将对Windows Sockets实现的描述拷贝到这个字符串中,包括制造商标识。文本(最多可以有256个字符)可以包含任何字符,但是要注意不能包含控制字符和格式字符,应用程序对其最可能的使用方式是把它(可能被截断)显示在在状态信息中。
szSystemStatus
以null结尾的ASCII字符串,Windows Sockets DLL把有关的状态或配置信息拷贝到该字符串中。Windows Sockets DLL应当仅在这些信息对用户或支持人员有用时才使用它们,它不应被作为szDescription域的扩展。
iMaxSockets
单个进程能够打开的socket的最大数目。Windows Sockets的实现能提供一个全局的socket池,可以为任何进程分配;或者它也可以为socket分配属于进程的资源。这个数字能够很好地反映Windows Sockets DLL或网络软件的配置方式。应用程序的编写者可以通过这个数字来粗略地指明Windows Sockets的实现方式对应用程序是否有用。例如,X Windows服务器在第一次启动的时候可能会检查iMaxSockets的值:如果这个值小于8,应用程序将显示一条错误信息,指示用户重新配置网络软件(这是一种可能要使用szSystemStatus文本的场合)。显然无法保证某个应用程序能够真正分配iMaxSockets个socket,因为可能有其它WindowsSockets应用程序正在使用。
iMaxUdpDg
Windows Sockets应用程序能够发送或接收的最大的用户数据包协议(UDP)的数据包大小,以字节为单位。如果实现方式没有限制,那么iMaxUdpDg为零。在Berkeley sockets的许多实现中,对于UDP数据包有个固有的限制(在必要时被分解),大小为8192字节。Windows
Sockets的实现可以对碎片重组缓冲区的分配作出限制。对于适合的WindowsSockets 实现,iMaxUdpDg的最小值为512。注意不管iMaxUdpDg的值是什么,都不推荐你发回一个比网络的最大传送单元(MTU)还大的广播数据包。(Windows
Sockets API 没有提供发现MTU的机制,但是它不会小于512个字节)。WinSock2.0版中已被废弃。
lpVendorInfo
指向销售商的数据结构的指针。这个结构的定义(如果有)超出了WindowsSockets规范的范围。
fd_set struct
fd_count dword ? ;fd_array中存放的套接字句柄数量
fd_array dword FD_SETSIZE DUP(?);套接字句柄列表
fd_set ends
Sockaddr_in
折叠sockaddr_in(在netinet/in.h中定义):
struct sockaddr_in {
short sin_family; /* Address family */
unsigned short sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
sin_family指代协议族,在socket编程中只能是AF_INET
sin_port存储端口号(使用网络字节顺序),在linux下,端口号的范围0~65535,同时0~1024范围的端口号已经被系统使用或保留。
sin_addr存储IP地址,使用in_addr这个数据结构
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
s_addr按照网络字节顺序存储IP地址
sockaddr_in和sockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向
sockaddr的结构体,并代替它。也就是说,你可以使用sockaddr_in建立你所需要的信息,
timeval STRUCT
tv_sec dword ? ;秒
tv_usec dword ? ;微秒
timeval ends