win32汇编实现一个简单的TCP服务端程序(WinSock的简单认知应用)

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

时间: 2024-10-11 13:02:56

win32汇编实现一个简单的TCP服务端程序(WinSock的简单认知应用)的相关文章

手写一个模块化的 TCP 服务端客户端

前面的博客 基于 socket 手写一个 TCP 服务端及客户端 写过一个简单的 TCP 服务端客户端,没有对代码结构进行任何设计,仅仅是实现了相关功能,用于加深对 socket 编程的认识. 这次我们对整个代码结构进行一下优化,使其模块化,易扩展,成为一个简单意义上的“框架”. 对于 Socket 编程这类所需知识偏底层的情况(OS 协议栈的运作机制,TCP 协议的理解,多线程的理解,BIO/NIO 的理解,阻塞函数的运作原理甚至是更底层处理器的中断.网卡等外设与内核的交互.核心态与内核态的切

编写一个简单的TCP服务端和客户端

下面的实验环境是linux系统. 效果如下: 1.启动服务端程序,监听在6666端口上  2.启动客户端,与服务端建立TCP连接  3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 什么是SOCKET(套接字): 百度的解释是:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket. 将计算机比作酒店,那么通过IP寻找主机,就好比通过地址寻址酒店.通过端

第5章-unix网络编程 TCP/服务端程序示例

这一章主要是完成一个完整的tcp客户/服务器程序.通过一很简单的例子.弄清客户和服务器如何启动,如何终止,发生了某些错误会发生什么.这些事很重要的 客户端代码 #include "unp.h" //static void str_cli1(FILE*fp,int sockfd); int main(int argc,char *argv[]) { int sockfd; struct sockaddr_in servaddr; sockfd=Socket(AF_INET,SOCK_ST

网络编程之TCP客户端开发和TCP服务端开发

开发 TCP 客户端程序开发步骤 创建客户端套接字对象 和服务端套接字建立连接 发送数据 接收数据 关闭客户端套接字 import socket if __name__ == '__main__': # 创建tcp客户端套接字 # 1. AF_INET:表示ipv4 # 2. SOCK_STREAM: tcp传输协议 tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 和服务端应用程序建立连接 tcp_c

[C语言]一个很实用的服务端和客户端进行TCP通信的实例

本文给出一个很实用的服务端和客户端进行TCP通信的小例子.具体实现上非常简单,只是平时编写类似程序,具体步骤经常忘记,还要总是查,暂且将其记下来,方便以后参考. (1)客户端程序,编写一个文件client.c,内容如下: #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <

一个简单的客户单与服务端程序

实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上  2.启动客户端,与服务端建立TCP连接  3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在<TCP/IP协议卷二>中,翻译时也是用 "插口" 来表示socket的.

python编程系列---tcp服务端的简单实现

流程如下: """tcp服务端创建流程1. 创建服务端的tcp socket : server_socket 用于监听客户端的请求2. 绑定端口3. server_socket开启监听,由主动连接模式变为被动接受模式4. 等待接收客户端的请求, 一有连接,则立即响应,且创建一个与客户端对接的socket,用该socket与客户端通信5. 使用新创建的socket与客户端通信6. 关闭新创建的socket, 该socket关闭,则与当前客户端结束通信7. server_sock

Linux socket编程入门(1)-TCP服务端

#include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define DEFAULT_PORT 8000 #define MAXLINE 4096 int main(int argc, char

Node.js是一个事件驱动I/O服务端JavaScript环境

Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎.目的是为了提供撰写可扩充网络程序,如Web服务.第一个版本由Ryan Dahl于2009年发布,后来,Joyent雇用了Dahl,并协助发展Node.js. 其他编程语言的类似开发环境,包含Twisted于Python,Perl Object Environment于Perl,libevent于C,和EventMachine于Ruby.与一般JavaScript不同的地方,Node.js并不是在Web浏