Win32汇编套接字聊天工具,前一阵子学完了套接字。应朋友要求写一个聊天程序(注释写全).部分完整以下。希望能给大家学习套接字提供一个完整的示例.功能在老罗实现的差不多,代码按照更容易理解的应用习惯写的!(^_^应该比老罗的更看得懂了吧)
代码:
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
include kernel32.inc
includelib user32.lib
includelib kernel32.lib
include ws2_32.inc
includelib ws2_32.lib
IDD_DIALOG1 EQU 101
IDC_NUM EQU 1000
TCP_PORT EQU 9999
SESSION struct
szSender db 12 dup(?);用户名
dwMessageId dd ?;已经取的消息ID
dwLastTime dd ?;链路的最后一次活动时间
SESSION ends
.data?
hInstance dword ?
hWinMain dword ?
dwSocketCount dword ?
hListenSocket dword ?
dwPos dword ?
F_STOP equ 01h
.data
szErr1 db '绑定端口9999失败!',0
szErr2 db '创建套接字失败!',0
szErr3 db '监听线程退出!',0
szErr4 db '发送登录消息回去!',0
szErr5 db '取数据包成功!',0
szSystem db '系统消息',0
szLogin db '进入聊天室!',0
szLogout db '退出聊天室!',0
.code
;数据包定义
SEND_LOGIN equ 01H
SEND_LOGIN_RESP EQU 81H
SEND_DATA_UP equ 02H
SEND_DATA_DOWN equ 82H
SEND_LINK equ 83H
DATA_HEAD struct
dwCmdId dw ?;命令ID
dwLength dd ?;整个数据的大小
DATA_HEAD ends
DATA_LOGIN struct
szUser db 12 dup(?);用户名
szPass db 12 dup(?);密码
DATA_LOGIN ends
DATA_LOGIN_RESP struct;服务器返回登录成功
dwResult db ?;0成功 1用户名或密码错误
DATA_LOGIN_RESP ends
DATA_UP struct ;
dwLength dd ? ;后面数据的长度
szContent db 256 dup(?);聊天内容
DATA_UP ends
DATA_DOWN struct
szUser db 12 dup(?) ;发送者
dwLength dd ? ;后面数据的长度
szContent db 256 dup(?);聊天内容
DATA_DOWN ends
DATA_STRUCT struct;数据结构
MsgHead DATA_HEAD <>
union
Login DATA_LOGIN <>
LoginResp DATA_LOGIN_RESP <>
MsgUp DATA_UP <>
MsgDown DATA_DOWN <>
ends
DATA_STRUCT ends
_WaitTime proc _hSock,_dwTime;按特定时间等待数据 输入参数_dwTime(微秒),超时返回0,有数据返回1,失败返回SOCKET_ERROR
local @stFdSet:fd_set,@stTimeval:timeval;select要使用的2个结构
mov @stFdSet.fd_count,1
push _hSock
pop @stFdSet.fd_array
mov @stTimeval.tv_sec,0
push _dwTime
pop @stTimeval.tv_usec;注意使用的是微秒
invoke select,0,addr @stFdSet,NULL,NULL,addr @stTimeval;返回值有3种可能 1.0没有数据到达 2.SOCKET_ERROR链路中断 3.返回1 已经有数据到达
ret
_WaitTime endp
_RecvData proc _hSock,_lpBuf,_len;接收指定大小的数据,输入参数_lpBuf(存放缓冲区),_len(接收的数据大小) 返回TRUE失败 返回FALSE成功
local @dwReturn,@dwTime;
pushad
mov @dwReturn,TRUE
mov ebx,_len
mov esi,_lpBuf
invoke GetTickCount
mov @dwTime,eax
@@:
invoke GetTickCount
sub eax,@dwTime
cmp eax,10*1000;如果超过了10秒就退出,接小数据给10秒足够
jge _Ret
invoke _WaitTime,_hSock,100*1000;等待100ms
cmp eax,SOCKET_ERROR
jz _Ret;出错就退出
cmp eax,0
jz @B ;如果数据还没有就绪则继续等待
invoke recv,_hSock,esi,ebx,0 ;注意这里是使用esi ebx 因为这2个数据我们下面还要经过计算
.if (eax==SOCKET_ERROR) || !eax;如果接收出错,或者接收数据为0肯定也是错的
jmp _Ret
.endif
.if eax<ebx
add esi,eax;把esi指向后字符后面去
sub ebx,eax;要接收的减去已经接收好的
jmp @B;继续接收余下的
.endif
mov @dwReturn,FALSE
;invoke MessageBox,NULL,addr szErr5,0,MB_OK
_Ret:
popad
mov eax,@dwReturn
ret
_RecvData endp
_RecvAll proc _hSock,_lpBuf,_len;接收符合规范的数据包,返回TRUE为失败
local @dwReturn
pushad
mov @dwReturn,TRUE
mov esi,_lpBuf
assume esi:ptr DATA_STRUCT
mov ecx,sizeof DATA_HEAD;先接收数据头
invoke _RecvData,_hSock,esi,ecx
or eax,eax
jnz _Ret;返回TRUE接收失败
mov ecx,[esi].MsgHead.dwLength
cmp ecx,_len
jg _Ret;如果要接收数据包大小都比实际要接收的还大,出错,
cmp ecx,sizeof DATA_HEAD
jb _Ret;如果要接收的数据头比固定的数据头还要小,出错
sub ecx,sizeof DATA_HEAD;实际接收的数据头-固定的数据头
add esi,sizeof DATA_HEAD;把指针指向实际接收好的数据头的最后一位
.if ecx;有的数据只有数据头没有数据体,所以这里要测试一下,如果ecx为0就表示只有数据头,也就不用再接收了
invoke _RecvData,_hSock,esi,ecx ;把余下的数据体接收完
.else
xor eax,eax;表示已经接收成功,只接收了数据头
.endif
mov @dwReturn,eax;因为_RecvData成功是返回FALSE,所以经过上面的2个分支中的一个都是成功的
;invoke MessageBox,NULL,addr szErr5,0,MB_OK
_Ret:
assume esi:nothing
popad
mov eax,@dwReturn
ret
_RecvAll endp
;消息队列开始
QUEUE_SIZE EQU 100H
QUEUE_ITEM struct
dwId dd ?;消息ID
szSender db 12 dup(?);发送消息的用户
szContent db 256 dup(?);消息内容
QUEUE_ITEM ends
.data?
stCS CRITICAL_SECTION <?>;临界区对象
stQueue QUEUE_ITEM QUEUE_SIZE dup (<?>);100个消息队列
dwMsgCount dd ?;当前的消息总数
.data
dwSe dd 1;消息队列从1开始
.code
_InsertQueue proc _lpszSender,_lpszContent;插入一条消息,如果消息已经满,则删除最前面的一条,再插入
pushad
invoke EnterCriticalSection,addr stCS;进入临界区对象
mov eax,dwMsgCount
.if eax>=QUEUE_SIZE ;如果现有的队列数大于等于最大的队列数,则移动数据。去掉最前面的一条消息
mov edi,offset stQueue
mov esi,offset stQueue+sizeof QUEUE_ITEM
mov ecx,(QUEUE_SIZE-1)*sizeof QUEUE_ITEM;99个消息队列的长度
mov eax,ecx
cld
rep movsb;传送数据
.else
mov ecx,sizeof QUEUE_ITEM;mov eax,dwMsgCount*sizeof QUEUE_ITEM这样写是错误的。因为mov不能计算带内存操作数
mul ecx
inc dwMsgCount;队列总数+1
.endif ;上面2个分支将会使eax是队列数据的偏移
mov esi,offset stQueue
assume esi:ptr QUEUE_ITEM
add esi,eax;esi再加上偏移就是指向将要插入的结构地址
invoke lstrcpy,addr [esi].szSender,_lpszSender
invoke lstrcpy,addr [esi].szContent,_lpszContent
inc dwSe
mov eax,dwSe
mov [esi].dwId,eax
assume esi:nothing
invoke LeaveCriticalSection,addr stCS;离开临界区
popad
ret
_InsertQueue endp
_GetQueue proc _dwId,_lpszSender,_lpszContent;取消息,入口参数_dwId(想取得的消息ID),如果消息ID比最小的ID还小则返回最小的ID消息,如果要取的消息ID
;比现有的消息ID都大,则不返回消息,取得消息返回TRUE 并且是消息ID 没取到消息返回FALSE
local @dwReturn
pushad
invoke EnterCriticalSection,addr stCS;进入临界区
xor eax,eax
mov @dwReturn,eax
cmp eax,dwMsgCount;如果现有的消息队列总数为0则退出。没有消息可以取
jz _Ret
mov eax,dwMsgCount;现有消息总数
mov esi,offset stQueue;指向第一条消息
assume esi:ptr QUEUE_ITEM
mov ecx,[esi].dwId;取得最小的消息ID
lea edx,[ecx+eax-1] ;最小的偏号+现有编号-1=现有最大的消息ID
mov eax,_dwId;要取的消息ID
cmp eax,ecx ;要取的消息ID比最小的还小就把消息ID改为现有最小的
jae @F
mov eax,ecx
@@:
cmp eax,edx
ja _Ret;消息ID比最大的还大。则没有消息可以取,退出子程序
;程序能走到这一步表示已经是开始取消息了,eax则是要取的消息ID。
mov @dwReturn,eax;返回真,表示取得消息
sub eax,[esi].dwId;要取的ID减去最小的,将是要取的消息偏移ID值
mov ecx,sizeof QUEUE_ITEM
mul ecx;eax就是要取的消息偏移
add esi,eax
invoke lstrcpy,_lpszSender,addr [esi].szSender
invoke lstrcpy,_lpszContent,addr [esi].szContent
assume esi:nothing
_Ret:
invoke LeaveCriticalSection,addr stCS;离开临界区
popad
mov eax,@dwReturn
ret
_GetQueue endp
assume esi:ptr DATA_STRUCT
assume edi:ptr SESSION
_Link proc uses esi edi _hSocket,_lpBuf,_lpSession;链路检测,成功返回FALSE 失败返回TRUE
invoke GetTickCount
push eax
sub eax,[edi].dwLastTime
cmp eax,30*1000;30秒
pop eax;记得啊。这个pop要在jb前面。不然可是程序错误哦
jb _Ret;没有到30秒的时间就不用发链路检测
mov [edi].dwLastTime,eax;更新链路时间
mov [esi].MsgHead.dwCmdId,SEND_LINK
mov [esi].MsgHead.dwLength,sizeof DATA_HEAD ;发送链路检测包,只有数据头,没有数据体
invoke send,_hSocket,esi,[esi].MsgHead.dwLength,0
cmp eax,SOCKET_ERROR
jnz _Ret
ret
_Ret:
xor eax,eax;
ret
_Link endp
;系统在空闲时间主动发送消息队列的消息到客户端,返回TRUE表示是错误 返回FALSE表示正确,并且有数据到达
_SendMsgQueue proc uses esi edi _hSocket,_lpBuf,_lpSesstion
mov esi,_lpBuf
mov edi,_lpSesstion
.while !(dwPos & F_STOP)
mov ecx,[edi].dwMessageId
inc ecx
invoke _GetQueue,ecx,addr [esi].MsgDown.szUser,addr [esi].MsgDown.szContent;发送消息
.break .if !eax
mov [edi].dwMessageId,eax;函数返回消息ID ,存进SESSION里面
invoke lstrlen,addr [esi].MsgDown.szContent;取内容长度
inc eax;注意呀注意,这里要+1.使最后面以0结尾。不然后面会有乱码
mov [esi].MsgDown.dwLength,eax;存进结构DATA_DOWN里面
add eax,sizeof DATA_HEAD + DATA_DOWN.szContent;这里计算总的数据长度 内容+数据头+DATA_DOWN结构到szContent字段的长度
mov [esi].MsgHead.dwLength,eax
mov [esi].MsgHead.dwCmdId,SEND_DATA_DOWN
invoke send,_hSocket,esi,eax,0
.break .if eax==SOCKET_ERROR
invoke GetTickCount
mov [edi].dwLastTime,eax
invoke _WaitTime,_hSocket,0;查看缓冲区是否有数据到达。如果有到达就不发送先退出去接收数据
.break .if eax==SOCKET_ERROR
.if eax
xor eax,eax;有数据就返回FALSE
.break
.endif
.endw
ret
_SendMsgQueue endp
_socketThread proc _lParam ;工作线程。每个用户登录都会创建一个此线程为用户工作
local @stSess:SESSION,@stBuf[512]:BYTE
pushad
lea esi,@stBuf
lea edi,@stSess
inc dwSocketCount
invoke SetDlgItemInt,hWinMain,IDC_NUM,dwSocketCount,FALSE
invoke RtlZeroMemory,addr @stSess,sizeof @stSess
push dwSe;把当前最新的消息编号给新用户
pop [edi].dwMessageId
;用户帐号的检测
invoke _RecvAll,_lParam,addr @stBuf,sizeof @stBuf
jnz _Ret
.if [esi].MsgHead.dwCmdId!=SEND_LOGIN;第一次的数据肯定是用户登录,不然就是错误的
jmp _Ret
.else
invoke lstrcpy,addr [edi].szSender,addr [esi].Login.szUser;取得用户名
mov [esi].LoginResp.dwResult,0;前面规定的。登录成功返回0
.endif
mov [esi].MsgHead.dwCmdId,SEND_LOGIN_RESP
mov [esi].MsgHead.dwLength,sizeof DATA_HEAD+sizeof DATA_LOGIN_RESP;服务器接收到了登录消息。将要回传登录消息给客户端
invoke send,_lParam,esi,[esi].MsgHead.dwLength,0;把登录回应发送出去
cmp eax,SOCKET_ERROR
jz _Ret
cmp [esi].LoginResp.dwResult,0
jnz _Ret;测试登录回应是不是0,我们规定是0才是成功,不是0就关闭工作线程
;登录广告
invoke lstrcpy,esi,addr [edi].szSender;放入用户帐号
invoke lstrcat,esi,addr szLogin;;再加上“进入聊天室"
invoke _InsertQueue,addr szSystem,esi;放入登录消息进消息队列
invoke GetTickCount
mov [edi].dwLastTime,eax
;开始循环工作
.while !(dwPos & F_STOP)
invoke _SendMsgQueue,_lParam,esi,edi;发送消息队列的消息
.break .if eax;如果发送消息队列里面失败会返回TRUE
invoke _Link,_lParam,esi,edi
.break .if eax;如果测试链路失败会返回TRUE
.break .if dwPos & F_STOP;退出标志是否设置
invoke _WaitTime,_lParam,200*1000;等待200ms.如果没有数据则循环
.break .if eax==SOCKET_ERROR
.if eax;如果有数据到达
invoke _RecvAll,_lParam,esi,sizeof @stBuf
.break .if eax;失败
invoke GetTickCount
mov [edi].dwLastTime,eax;更新活动时间,比如忙的时候就不需要发送链路检测包了
.if [esi].MsgHead.dwCmdId==SEND_DATA_UP;如果是聊天信息。则放入消息队列
invoke _InsertQueue,addr [edi].szSender,addr [esi].MsgUp.szContent
.ENDIF
.endif
.endw
;退出广播
invoke lstrcpy,esi,addr [edi].szSender
invoke lstrcat,esi,addr szLogout
invoke _InsertQueue,addr szSystem,esi;放入退出消息进消息队列
_Ret:
;invoke MessageBox,NULL,addr szErr3,0,MB_OK
invoke closesocket,_lParam
dec dwSocketCount
invoke SetDlgItemInt,hWinMain,IDC_NUM,dwSocketCount,FALSE
popad
ret
_socketThread endp
assume esi:nothing,edi:nothing
_Listen proc _lParam
local @stAddr:sockaddr_in
invoke RtlZeroMemory,addr @stAddr,sizeof @stAddr;
mov @stAddr.sin_family,AF_INET
mov @stAddr.sin_addr,INADDR_ANY ;接收本机所有IP
invoke htons,TCP_PORT ;把端口转换成网络字节顺
MOV @stAddr.sin_port,ax
invoke socket,AF_INET,SOCK_STREAM,NULL
.if eax==INVALID_SOCKET;创建套接字失败
invoke MessageBox,0,addr szErr2,0,MB_OK
jmp _Ret
.endif
mov hListenSocket,eax
invoke bind,hListenSocket,addr @stAddr,sizeof @stAddr
.if eax;梆定失败
invoke MessageBox,NULL,addr szErr1,0,MB_OK
jmp _Ret
.endif
invoke listen,hListenSocket,5;监听
.while TRUE
invoke accept,hListenSocket,NULL,NULL;接受请求
.break .if eax==INVALID_SOCKET;失败退出线程
push ecx
invoke CreateThread,NULL,0,addr _socketThread,eax,NULL,esp;创建工作线程
pop ecx
invoke CloseHandle,eax
.endw
_Ret:
ret
_Listen endp
_DiaProc proc uses ebx esi edi hWnd,uMsg,wParam,lParam
local @stWD:WSADATA
mov eax,uMsg
.if eax==WM_INITDIALOG
push hWnd
pop hWinMain
invoke InitializeCriticalSection,addr stCS;初始化临界区
invoke WSAStartup,0002H,addr @stWD;第二参数是16位,指定版本号 高位是副版本号。低位是主版本号
push ecx
invoke CreateThread,NULL,0,addr _Listen,NULL,NULL,esp;创建监听线程
pop ecx
invoke CloseHandle,eax
.elseif eax==WM_CLOSE
invoke closesocket,hListenSocket;关闭监听套接字
or dwPos,F_STOP;设置退出标志
.while dwSocketCount;循环等待所有工作线程退出
.endw
invoke DeleteCriticalSection,addr stCS;释放临界区
invoke WSACleanup;释放套接字函数
invoke EndDialog,hWnd,NULL
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_DiaProc endp
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_DIALOG1,NULL,offset _DiaProc,NULL
invoke ExitProcess,0
end start