23.1 Windows Socket接口简介
(1)TCP/IP模型
①TCP/IP的核心协议运行于传输层和Internet层,主要包括TCP、UDP和IP协议,而TCP协议和UDP协议是以IP协议为基础而封装的。这两种协议提供了不同方式的数据通信服务。
②IP协议比喻为道路,则下一层的网络访问层上的协议相当于不同的铺路材料,上面的TCP和UPD协议相当于路上跑的不同类型的车辆,再上层应用层的协议相当于车上的丰富多彩的货物。他们都是以TCP、UDP为载体的。
(2)WinSock动态库
①早期的1.1版的WinSock接口最后也是调用2.0版的WS2_32.dll文件的
②使用前须包含头文件#include Winsock2.h和增加导入库Ws2_32.lib
(3)加载和释放动态链接库
①WSAStartup函数:WSAStartup(wVersionRequested, lpWSAData)
参数 |
含义 |
WORD wVersionRequested |
指定动态库的版本号。如2.0版时0x0002(MAKEWORD(2,0)) |
LPWSADATA lpWSAData |
指向WSADATA结构体,用来返回动态链接库的详细信息。 wVersion:库文件建议应用程序使用的版本 wHighVersion:库文件支持的最高WinSock版本 szDescription:返回库描述字符串,如“WinSock2.0”之类的。 szSystemStatus:系统状态字符串:返回如“Runing”之类的状态 iMaxSockets:同时支持的最大套接字数量 iMaxUpdDg: 2.0版中己废弃的字段 lpVendorInfo:2.0版中己废弃的字段 |
返回值 |
如果装入成功,返回0。否则,返回出错代码: WSASYSNOTREADY:网络子系统未准备好 WSAVERNOTSUPPORTED:不支持指定的版本 WSAEINPROGRESS::另一个阻塞方式的WinSock1.1操作正在进行中 WSAEPROCLIM:WinSock接口己达到所支持的最大任务数 WSAEFAULT:输入参数lpWSAData指定的指针无效 ★该函数出错时直接返回出错代码,因为库还没装入,无法使用WSAGetLastError函数。其他WinSock函数出错时返回SOCKET_ERROR或INVALID_SOCKET,要进一步得到出错代码,须调用WSAGetLastError函数来获取。 |
②释放WinSock:int WSACleanup(void);//返回值成功为0,否则为SOCKET_ERROR
23.2 Windows Socket接口的使用
23.2.1 IP地址的转换
(1)IP地址和端口:
①IP地址(32位):如11000000.10101000.00000001.01100100(192.168.1.100)
②端口(16位):即端口数量为65536个。
③TCP协议和UDP协议是两个完全独立的模块,两者的工作互不相干,所以TCP和UDP各自的端口号也相互独立,即一个进程使用TCP协议的某个端口号并不影响另一进程使用UDP协议的同名端口号。但同一协议的同一端口号无法同时被两个进程同时使用。
(2)常用协议和应用程序使用的默认端口号
协议或应用程序 |
TCP端口号 |
UDP端口号 |
FTP |
21 |
|
Telnet |
23 |
|
SMTP |
25 |
|
HTTP |
80 |
|
POP3 |
110 |
|
DNS查询 |
53 |
|
TFTP协议 |
69 |
|
NetBIOS名字服务 |
137 |
|
NetBIOS数据包服务 |
138 |
|
SQLServer数据库 |
139、1433 |
|
Oracle数据库 |
1521 |
(3)sockaddr_in结构体:由于TCP和UDP协议必须同时指定IP和端口号。(封装之!)
字段 |
含义 |
short sin_family |
地址族,指明互联网的地址类型。在WinSock中必须为AF_INET |
unsigned short sin_port |
端口号(使用网络字节顺序),如53端口号,则等于htons(53); |
struct in_addr sin_addr |
IP地址(使用网络字节顺序),如 sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); sin_addr.S_un.S_addr=htonl(INADDR_ANY); |
char sin_zero[8]; |
空字节 |
23.2.2 网络字节顺序——大端模式
(1)小端模式:低位放低地址,高位放高地址。如Intel80x86系列的处理器
(2)大端模式:低位放高地址,高位放低地址。如RISC芯片、网络字节。如0x12345678,则依次送入端口中号的数据为0x12、0x34、0x56、0x78(从低地址开始发送)
23.2.3 字节顺序转换函数
函数 |
说明 |
htons、htonl |
将16(或32)位的当前主机字节顺序数据转为网络顺序 |
ntohs、ntohl |
将16(或32)位的网络顺序的数据转为当前主机字节顺序 |
inet_addr |
将字符串转为IP地址,如inet_addr(“127.0.0.1”); |
inet_ntoa |
将IP转为字符串。如inet_ntoa(sa.sin_addr) |
23.2.4 套接字
(1)什么是套接字
①建立用来通信的对象,是“通信的一端”
②套接字的种类:流套接字(stream socket)、数据报套接字(datagram socket)、原始套接字(raw socket)、可靠信息分递套接字(rdm socket)、连续小分包套接字(seqpacket socket)。
(2)套接字的创建和关闭
①创建套接字:SOCKET socket(af, type, protocol)
参数 |
含义 |
int af |
用来指定套接字使用的地址格式,和sockaddr_in中的sin_family的定义是一样的。唯一可使用的值是AF_INET。 |
int type |
用来指定套接字的类型 SOCK_STREAM——流套接字,使用TCP协议提供有连接和可靠的传输 SOCK_DGRAM——数据报套接字,使用UDP协义提供无连接的不可靠的传输 SOCK_RAW——原始套接字,WinSock接口并不使用某种特定的协议去封装它,而是由程序自行处理数据包,以及协议首部,正因为如此,所以可以使用特殊的功能,如伪造发送者地址等。 |
int protocol |
当type指定为SOCK_RAW时,protocol可指定以下的值 ①IPPROTO_IP、IPPROTO_ICMP、IPPROTO_TCP、IPPROTO_UDP:分别指定使用IP、ICMP、TCP和UDP协议。这时会自动为数据加上IP首部。并且将IP首部中的上层协议字段设置为指定的这些协议的名称。但是使用这个套接字接收数据时,系统却不会将IP首部自动去除,需要自行处理。 ②IPPROTO_RAW:系统将数据包直接送到网络访问层,程序需要自己添加IP首部及其他协议的首部,并用需要自己计算和填充协议首部中的检验和字段。但这个socket只能用来发送数据包而无法接收数据。 |
②关闭套接字:int closesocket(SOCKET s);
(3)套接字的工作模式:阻塞(创建时默认的工作方式)和非阻塞
23.2.5 网络应用程序的一般流程
(1)TCP协议的特征和TCP程序的工作流程
①特点:面向连接、可靠的字节流服务
面向连接 |
①两个TCP套接字在开始传输数据之前必须先建立一个连接。(犹如打电话) ②TCP协议不能用于广播 |
字节流服务 |
传输入数据是流方式的,没有边界。如发送方分3次发送100、150、200字节的数据包。对接收来说,无法知道数据如何分割,可以一次接收450字节,也可以分10次接收,每次接收45字节。 |
可靠 |
①采用超时及重传机制保证不丢失数据。每发送一个数据包,会启动一定时器,等待对方确认收到这个包。如果指定时间内没得到确认,会重发这个数据包。 ②如果接收方发现数据包校验有错,TCP协议丢弃这个数据包,并且不发送确认,从而发送方因收不到确认而重发这个数据包。 |
②数据包在传输的时候会通过多个路由器,不同数据包到达终点的先后顺序可能与发送数据包的先后顺序不同。但这没关系,因为TCP协议首部保存数据包的序号。如有必要,在收到数据时TCP协议会重新排序,并将正确的顺序交给应用程序。
③接收方收到的数据包有可能重复,原因之一是发送和确认之间有个时差,发送方可能因超时而重发数据,对于这种情况,接收方会丢弃重复的数据。
④TCP协议还提供流量控制机制,发送方可根据接收方应答时间和速率来调整数据的发送速度。防止速度太快,使接收方出现缓冲区溢出。
★注意:客户端无需绑定IP,只需向服务端发起连接请求。服务端处于监听状态。
(2)UDP协议的特征和UDP程序的工作流程
①特点:是一个无连接的,面向消息的,不可靠的传输层协议。
无连接 |
①客户端在发送UDP数据包前不需要先与服务器端进行握手确认。无法确认对方是否在线、也无法确认对方指定的端口是否在监听。属于“发出就不管”的协议 ②同一个UDP套接字可以向任何服务器地址发送数据,而无须创建多个套接字,即可采用广播方式。 |
面向消息 |
UDP数据包是有边界保护的。如发送方分三次分别发送100、150、200字节的UDP数据包,接收方必须分三次接收这些数据包。各个数据包之间的数据不会粘连。 |
不可靠 |
UDP协议并不对数据的可靠性与有序性等进行控制。 |
②TCP协议像打电话,而UDP协议像寄信。发信人虽然知道收信人的地址,但他并确定信是否会被收到。如果发了好几封信,在收信人回信之前,发信人也无法确定信件是否安全、无损和有序的到达。
③UDP协议不对数据进行可靠性保证,因此传输的效率较高。经常用在在线视频的传送。
★注意:客户端不必连接,直接发送数据。服务端也不必进入监听状态。客户端与服务端的唯一区别就是服务端必须首先将套接字绑定到一个固定端口。以便客户端能向约定的端口发送数据。
23.2.6 监听、发起连接和接收连接
(1)TCP客户端——连接到服务器:connect函数
参数 |
含义 |
SOCKET s |
TCP套接字的句柄 |
const struct sockaddr FAR *name |
指向一个sockaddr_in结构,用来指定服务器端的地址和端口 |
int namelen |
指定的sockaddr_in结构的长度 |
返回值 |
①阻塞模式下:成功返回0,否则SOCKET_ERROR。要知道详细原因,可调用WSAGetLastError函数。 常见错误: WSAECOONNERREFUSED:服务器没有在指定端口监听。 WSA_ETIMEDOUT:网络不通,或服务器不在线 ②非阻塞模式:均返回SOCKET_ERROR,但并不意味着连接失败,而是指函数返回里连接尚末成功。要调用WSAGetLastError得到出错代码。只是WSAEWOULDBLOCK才表示连接失败。 |
★客户端发起连接时,系统会自动为套接字选择一个空闲的端口,如果一定要用特定的端口连接服务器,可在调用connect前用bind函数来指定端口。
(2)TCP服务器端——在指定的IP地址和端口监听并接收连接
①绑定IP和端口:int bind(SOCKET s, const struct sockaddr FAR *name,int namelen );
参数 |
含义 |
SOCKET s |
TCP套接字的句柄 |
const struct sockaddr FAR *name |
指向一个sockaddr_in结构,用来指定需要绑定的服务器端的地址和端口。sin_addr字段的设置: INADDR_ARRAY(0):自动在本机的所有IP地址上监听 //如本机有3个网卡,配置3个IP //那么会自动在监听3个地址上监听 指定为内网IP:在指定的那个地址上进行监听 |
int namelen |
指定的sockaddr_in结构的长度 |
返回值 |
绑定成功,返回0。否则返回SOCKET_ERROR。一般是端口是被其他程序占用,出错代码为WSAEADDRINUSE。如果套接字己经绑定过了,返回WSAEFAULT。 |
②监听:int listen(SOCKET s, int backlog);
参数 |
含义 |
SOCKET s |
TCP套接字的句柄 |
int backlog |
监听队列中允许保持的尚未处理的最大连接数量。当套接字监听到客户端连接请求时,还需要调用accept才能建立真正的连接。在调用accept之前,连接请求会被保留在队列 中,如果这时另一个客户端也发起连接的话,这个连接也会被保留在队列里。Backlog指的就是这个队列最大的长度。 |
返回值 |
成功,返回0。这里套接字处于等待连接进入的状态。失败返回SOCKET_ERROR。如果没有bind操作就去listen,这里的出错代码是WSAINVAL。 |
③接受连接:SOCKET accept(SOCKET s, struct sockaddr FAR *addr,int FAR *addrlen);
参数 |
含义 |
SOCKET s |
监听中的套接字句柄 |
struct sockaddr FAR *addr |
addr指向一个缓冲区,函数会在这里返回一个sockaddr_in结构。结构中存放有连接请求方的IP地址和端口(即客户端的IP和端口)。可以通过这个参数,对客户端进行认证,如果检测到IP不合法,则调用closesocket关闭这个新套接字。如果不需要得到对方的地址信息,addr和addrlen都设为NULL。 |
addrlen |
指向一个int型的变量,函数在这里放入返回到上述结构长度。 |
返回值 |
如果成功,函数新建一个TCP套接字,这个新的套接字才是用来和该客户端连接。原来的套接字仍保持着监听状态。当要断开与客户端的连接时,也是要对这个新的套接字调用closesocket。 如果失败,返回INVALID_SOCKET。 |
【典型的accept处理】
while(TRUE) { SOCKET sc=accept(hListenSocket,NULL,0); if (sc==INVALID_SOCKET) break; //在这里创建一个新线程,对新套接字进行通信,以便马上能处理新连接。 //但这里直接对新连接进行数据收发,因为其他客户的连接请求可能没办法及时处理。 //新套接字可以通过lParam参数传递线程函数。 //可以用其他线程中closesocket这个监听套接字,表示不再进行监听。这样accept //会返回INVALID_SOCKET,这样程序就可以退出循环。(注意关闭的是监听套接字) }
23.2.7 数据的收发
TCP一旦连接(对客户端来说是connect返回成功,对服务器端来说是accept返回新套接字)。那么连接双方是对等的,因为TCP连接是一个全双工的连接。任何一方可以在任何时刻向对方发送数据。
(1)使用TCP套接字收发数据
①发送数据:int send(SOCKET s, const char FAR *buf,int len, int flags);
参数 |
含义 |
SOCKET s |
指定套接字句柄 |
const char FAR *buf |
指向要发送的数据缓冲区 |
int len |
指定发送的数据长度 |
int flags |
一般默认为0 |
返回值 |
发送失败,返回SOCKET_ERROR。否则返回发送的字节数。 (注意:WinSock会为每个套接字分一个发送缓冲区和接收缓冲区,用send发送数据时,并不马上在网络上传递,而是先发送到“发送缓冲区”,WinSock会在合适的时候将数据发送出去,所以前面的“成功发送”指的是放入“发送缓冲区”而己)。 |
【扩展“发送缓冲区”】设send函数中要求发送为n字节,发送缓冲区的空闲空间m字节
工作模式 |
表现 |
备注 |
阻塞模式 |
A、如果发送缓冲区足够大(m≥n),数据放入缓冲区,函数马上返回。 B、如果发送缓冲区不够大(m<n),函数会一直等到全部数据放入缓冲区才返回。 |
1、在阻塞模式下,函数在发送完后才返回,但返回值是实际发送的字节数n。 2、非阻塞模式下,函数会立即返回,返回值是函数实际发送的字节,介于(1到n)之间。要利用循环,多次调用send函数。 |
非阻塞模式 |
A、如果发送缓冲区足够大(m≥n),数据放入缓冲区,函数马上返回。返回值为为实际发送的字节数n B、如果发送缓冲区不够大(n>m>0)将直接将m字节放入缓冲区,然后返回,这里的返回值为发送的实际字节数m; C、这里缓冲区满(即m=0),函数返回SOCKET_ERROR,再获取出错代码时会返回WSAEWOULDBLOCK。 |
②接收数据:int recv(SOCKET s, char FAR *buf, int len, int flags);
参数 |
含义 |
SOCKET s |
指定读取的套接字句柄 |
char FAR *buf |
用来返回数据的缓冲区 |
int len |
指定缓冲区的大小 |
int flags |
指定读取时的选项,可以是: MSG_PEEK——返回数据后并不从缓冲区清除数据。 MSG_OOB——发送或接收带外数据(表示重要数据)。如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方 MSG_WAITALL——等待所有数据 |
返回值 |
接收失败,返回SOCKET_ERROR。否则返回实际接收的字节数。 (注意:WinSock会为每个套接字分一个发送缓冲区和接收缓冲区,用send发送数据时,并不马上在网络上传递,而是先发送到“发送缓冲区”,WinSock会在合适的时候将数据发送出去,所以前面的“成功发送”指的是放入“发送缓冲区”而己)。 |
【扩展“接收缓冲区”】设recv函数接收为n字节,当前接收缓冲区有m字节数据
工作模式 |
表现 |
备注 |
阻塞模式 |
A、如果接收缓冲区为空(即m=0),函数会等待直至有数据到达为止。 B、如果缓冲区己经有m字节数据,则: 当m≥n时,函数从缓冲区读n个字节并返回; 当m< n时,那么只读取m个字节数据并马返回。 |
1、两种模式下返回成功时(返回值不是SOCKET_ERROR)时: ①函数返回的是实际接收的字节数,这个值在1<到n之间,也就是在不超过n的前提下,有多少返回多少。 ②如果要接收到指定数量的字节(n),即应通过循环接收。 2、如果返回SCOKET_ERROR: ①阻塞模式意味着连接己经因各种情况而断开。 ②非阻塞模式下,要继续调用WSAGetLastError获取出错代码。如果出错代码为WSAEWOULDBLOCK,意味着缓冲区为空。否则表示连接己断开。 |
非阻塞模式 |
A、当接收缓冲区中己经有数据的情况,表现与阻塞模式一样,函数马上返回。 B、在接收缓冲区为空(m=0)时,函数不会等待,也是马上返回。但返回值为SOCKET_ERROR,再获取出错代码时会返回WSAEWOULDBLOCK。 |
(2)使用UDP套接字收发数据:UDP套接字创建后,就可直接向服务器收送数据了。
①发送UDP数据包:sendto函数——int sendto(SOCKET s, const char FAR *buf, int len, int flags,const struct sockaddr FAR *to, int tolen);
A、参数to:指向一个包含目标地址和端口号的sockaddr_in结构,tolen指定了这个结构体的大小。
B、UDP数据包有最大尺寸SO_MAX_MSG_SIZE的限制。如果数据包小于超过该尺寸,且发送成功,函数会返回实际发送数据的字节数;如果数据包超过该尺寸,函数将返回失败,这时没有任何数据被发送,并且出错代码是WSAEMSGSIZE。
C、阻塞模式与非阻塞模式下的sendto表现不同:
如果没有足够的发送缓冲区:阻塞模式下,函数将等待到缓冲区足够大为止;而非阻塞模式下会马上返回SOCKET_ERROR,这时得到的出错代码为WSAEWOULDBLOCK。
因UDP是面向消息的,数据包不会被割裂发送,所以不管哪种情况下,函数不会只发送部分数据。
D、sendto发送UDP数据包时,如果在调用sendto函数前bind了IP和端口号,则按指定的方式发送。如果没有绑定,系统会在第一次调用sendto时为该socket自动分配一个空闲端口,以后一直使用这个端口来发送。(注意一定要有IP和端口号了才能接收数据!)
②接收UDP数据包:recvfrom函数——int recvfrom(SOCKET s, char FAR* buf, int len, int flags,struct sockaddr FAR *from, int FAR *fromlen );
A、参数from指定了用来接收发送方地址的sockaddr_in结构体。可以从这个结构体中得到发送方的IP地址和端口。如果需要回复的话,可以根据这个地址进行回复。
B、同一个UDP套接字可以接收任何客户端发送过来的UDP数据包。只要对方指定了正确的IP和端口。该UDP就是专门负责用来接收来自该端口号的UDP数据包的。
C、UDP是面向消息的,当指定缓冲区尺寸小于接收缓冲区中的UDP包的尺寸,那么多余的部分数据会丢失,这里函数返回SOCKET_ERROR,出错代码为WSAMSGSIZE。如果大于缓冲区中UDP包的大小时,会接收该UDP包。但不会将后面到达的数据包内容一并返回。因为UDP包是有边界的,这里函数返回实际接收的数据大小。
D、阻塞模式与非阻塞模式下recv表现不同
阻塞模式下,如果接收缓冲区没有数据到达,函数会等待有数据包到达为止。
非阻塞模式下,函数会马上返回SOCKET_ERROR,出现代码为WSAEWOULDBLOCK。
(3)select函数及作用——用来检测套接字的各种情况
①当阻塞时,recv会等待对方发送数据而将本身所在的线程挂起。可以先用select函数检则是否有数据到达,如果有才去recv。如果没有,则线程继续执行下去。
②可以检测套接字是否有数据到达(即可读)、或套接字是否可写、或异常(如断开连接)。
③select函数:int select(nfds,lpreadfds,lpwritefds,lpexcept,lptimeout);
参数 |
含义 |
int nfds |
等于0,是为了兼容UNIX Socket而设置的 |
fd_set* lpreadfds |
①fd_set结构体: u_int fd_count; 存放要检测的套接字的数量 SOCKET fd_array[FD_SETSIZE]; //套接字句柄列表 ②lpreadfds:要检测的套接字是否可读(接收缓冲区数据是否有数据) ③lpwritefds:要检测的套接字是否可读(即发送缓冲区是否为空。 ④lpexcept:要检测的套接字是否出错(如连接是否断掉) |
fd_set* lpwritefds |
|
fd_set* lpexcept |
|
const struct timeval *lptimeout |
①timeval结构体: long tv_sec; //秒数 long tv_usec;//微秒(注意不是毫秒) ②如果lptimeout为NULL表示永远等待下去,直到列表中某个套接字就绪才返回 ③如果lptimeout结构中的时间为0,表示不管有没有套接字就绪马上返回。 ④如果lptimeout结构中的时间不为0,在指定时间内还没有套接字就绪,就超时返回。如果指定时间内有套接字就绪,则马上返回。 |
返回值 |
①因超时而返回时,返回0 ②因出错而返回时,返回SOCKET_ERROR。 ③因某个套按字就绪而返回,返回值是就绪套接字的数量。 |
【TcpEcho程序】一个简单的TCP服务端程序
/*----------------------------------------------------------------- TCPECHO.C —— 一个简易的TCP服务器程序(将收到的字符发回给客户端) (c)浅墨浓香,2015.6.24 -----------------------------------------------------------------*/ #include <Windows.h> #include "resource.h" #pragma comment(lib,"Ws2_32.lib") //typedef struct //{ // HWND hwnd; // int iTcp_Port; // SOCKET hSocket; //}SOCKETPARAM; #define TCP_PORT 9999 //监听端口 #define F_STOP 1 TCHAR szAppName[] = TEXT("TcpEcho"); int g_iThreadCount = 0; HWND g_hwnd = NULL; //对话框句柄 int g_dwFlag=0; //退出标志 BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { DialogBox(hInstance, TEXT("TCPECHO"), NULL, DlgProc); return 0; } //通信服务线程,每个客户端登录的连接将产生一个线程 DWORD WINAPI ServiceThread(PVOID pVoid) { SOCKET SrvSocket = (SOCKET)pVoid; char szBuffer[512]; FD_SET fds; TIMEVAL tv; int iRet; ++g_iThreadCount; SetDlgItemInt(g_hwnd, IDC_COUNT, g_iThreadCount, FALSE); while (!(g_dwFlag & F_STOP)) { /* *select函数用于判断套接字是否有可读、可读或是否有异常. *如果检测的就绪时,会返回就数据套接字的数量。 *如果因超时而返回时,返回值是0, *如果因异常(如连接断开)时,返回SOCKET_ERROR */ fds.fd_count = 1; fds.fd_array[0] = SrvSocket; tv.tv_usec = 200 * 1000; //200ms tv.tv_sec = 0; iRet = select(0, &fds, NULL, NULL, &tv); //因默认recv和send是阻塞的,用该函数来检测 //当套接字可读或可写时再去recv或send以防止 //该线程被阻塞,因为一旦被阻塞,当用户关闭 //应用程序时,g_dwFlag退出标志就无法被检测 //到,而如果客户端没发送数据时,会使该线程 //无法退出。所以用select加以检测。(很重要!) if (SOCKET_ERROR ==iRet) break; if (iRet) { if (SOCKET_ERROR == recv(SrvSocket, szBuffer, sizeof(szBuffer), 0)) break; if (SOCKET_ERROR == send(SrvSocket, szBuffer, iRet, 0)) break; } } closesocket(SrvSocket); --g_iThreadCount; SetDlgItemInt(g_hwnd, IDC_COUNT, g_iThreadCount, FALSE); return TRUE; } //监听线程 DWORD WINAPI ListenThread(PVOID pVoid) { SOCKET ServiceSocket,ListenSocket; SOCKADDR_IN sa; HANDLE hThread; TCHAR szErrorBind[] = TEXT("无法绑定到TCP端口9999,请检查是否有其它程序在使用!"); //创建socket ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); *(SOCKET*)pVoid = ListenSocket; //绑定socket memset(&sa, 0, sizeof(SOCKADDR_IN)); sa.sin_port = htons(TCP_PORT); sa.sin_family = AF_INET; sa.sin_addr.S_un.S_addr = INADDR_ANY; if (bind(ListenSocket, (PSOCKADDR)&sa, sizeof(SOCKADDR_IN))) //返回0表示无错误,是成功的。 { MessageBox(g_hwnd, szErrorBind, szAppName, MB_OK | MB_ICONSTOP); closesocket(ListenSocket); return FALSE; //ExitProcess(0); } //开始监听,等待连接并为每个连接创建一个新的服务线程 listen(ListenSocket, 5); while (TRUE) { ServiceSocket = accept(ListenSocket, NULL, 0); if (ServiceSocket == INVALID_SOCKET) break; hThread = CreateThread(NULL, 0, ServiceThread, (LPVOID)ServiceSocket, 0, 0); CloseHandle(hThread);//线程是内核对象,关闭表示不需用操作了(如唤醒、挂机)。 } closesocket(ListenSocket); return TRUE; } BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { WSADATA WSAData; static SOCKET ListenSocket; static HANDLE hListenThread; switch (message) { case WM_INITDIALOG: g_hwnd = hwnd; //载入WS2_32.DLL动态链0x0002:MAKEWORD(2,0) WSAStartup(MAKEWORD(2, 0), &WSAData); //动态库的信息返回到WSAdata变量中 //创建监听线程 hListenThread = CreateThread(NULL, 0, ListenThread, (LPVOID)&ListenSocket, 0, 0); CloseHandle(hListenThread); //只是关闭了一个线程句柄对象,表示我不再使用该句柄,即不对这个句柄对 //应的线程做任何干预了(如挂起或唤醒)。并没有结束线程。 return TRUE; case WM_CLOSE: closesocket(ListenSocket); //当未有客户端连接时,该socket在线程中创建,且未退出线程。 //所以要在这里监听socket,此时会将accept返回失败,监听线程退出。 g_dwFlag |= F_STOP; //设置退出标志,以便让服务线程中止 while (g_iThreadCount > 0); //等待服务线程关闭 WSACleanup(); EndDialog(hwnd, 0); return TRUE; } return FALSE; }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 TcpEcho.rc 使用 // #define IDC_COUNT 1001 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1002 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//TcpEcho.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Dialog // TCPECHO DIALOGEX 0, 0, 165, 36 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "TcpEcho服务器" FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN LTEXT "当前连线的客户端数量:",IDC_STATIC,15,15,89,8 LTEXT "0",IDC_COUNT,109,15,44,8 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO BEGIN "TCPECHO", DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 158 TOPMARGIN, 7 BOTTOMMARGIN, 29 END END #endif // APSTUDIO_INVOKED #endif // 中文(简体,中国) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED