Masimaro's blog - 网络编程
type: channel
link: https://www.masimaro.top/tag/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/
items: Java 网络编程
items: VC++ IPv6的支持
items: WinSock2 API
items: WinSock 完成端口模型
items: WinSock WSAEventSelect 模型
items: select模型
items: socket模型处理多个客户端

Java 网络编程
type: item
date: 2019-09-08T15:34:00+08:00
description: Java 中网络编程接口在java.net 包中在使用C/C++进行网络编程时，针对TCP Server端需要这些操作创建SOCKET绑定监听接受连接收取数据包发送数据包TCP Client端需要这些操作创建SOCKET连接Server端发送数据包读取响应包Java中针对Server 端和Client端分别提供了两个类 ServerSocket 和 SocketServerScoket 使用步骤如下:创建ServerSocket 对象并传入一个端口号到构造函数中。在构造的时候会自动创建Socket对象并执行绑定端口、监听端口的操作调用对象的 accept 方法等待连接调用对象的 getInputStream 和 getOutputStream 获取输入输出流，并通过输入输出流来进行收发数据在不用时调用 close 方法关闭套接字Socket 类使用步骤如下:创建 Socket 对象调用 connet 方法连接到指定服务器端口(或者在构造时传入服务器和端口进行连接)调用对象的 getInputStream 和 getOutputStream 获取输入输出流，并通过输入输出流来进行收发数据在不用时调用 close 方法关闭套接字一个普通的TCP通信的实例如下:import java.net.ServerSocket; import java.net.Socket; import java.io.IOException; import java.io.OutputStream; import java.io.InputStream; public class TCPServer{ public static void main(String[] args) throws IOException{ ServerSocket server = new ServerSocket(6666); Socket socket = server.accept() ; InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); byte[] bytes = new byte[1024]; int len = is.read(bytes); os.write(("echo:" + new String(bytes, 0, len)).getBytes()); server.close(); socket.close(); } }import java.net.Socket; import java.io.IOException; import java.io.OutputStream; import java.io.InputStream; public class TCPClient{ public static void main(String[] args)throws IOException{ Socket socket = new Socket("127.0.0.1", 6666); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); os.write("hello world!".getBytes()); byte[] bytes = new byte[1024]; int len = is.read(bytes); System.out.println(new String(bytes, 0, len)); socket.close(); } }下面是一个使用TCP协议进行文件传输的例子:import java.net.Socket; import java.io.IOException; import java.io.FileOutputStream; import java.io.InputStream; import java.net.ServerSocket; public class FileUploadServer{ public static void main(String[] args) throws IOException{ ServerSocket server = new ServerSocket(6666); FileOutputStream fos = new FileOutputStream("1.avi"); Socket socket = server.accept(); InputStream is = socket.getInputStream(); byte[] buf…

VC++ IPv6的支持
type: item
date: 2018-11-24T14:51:00+08:00
description: 最近根据项目需要，要在产品中添加对IpV6的支持，因此研究了一下IPV6的相关内容，Ipv6 与原来最直观的改变就是地址结构的改变，IP地址由原来的32位扩展为128，这样原来的地址结构肯定就不够用了，根据微软的官方文档，只需要对原来的代码做稍许改变就可以适应ipv6。修改地址结构Windows Socket2 针对Ipv6的官方描述根据微软官方的说法，要做到支持Ipv6首先要做的就是将原来的SOCKADDR_IN等地址结构替换为SOCKADDR_STORAGE 该结构的定义如下:typedef struct sockaddr_storage { short ss_family; char __ss_pad1[_SS_PAD1SIZE]; __int64 __ss_align; char __ss_pad2[_SS_PAD2SIZE]; } SOCKADDR_STORAGE, *PSOCKADDR_STORAGE;ss_family：代表的是地址家族，IP协议一般是AF_INET, 但是如果是IPV6的地址这个参数需要设置为 AF_INET6。后面的成员都是作为保留字段，或者说作为填充结构大小的字段，这个结构兼容了IPV6与IPV4的地址结构，跟以前的SOCKADDR_IN结构不同，我们现在不能直接从SOCKADDR_STORAGE结构中获取IP地址了。也没有办法直接往结构中填写IP地址。使用兼容函数除了地址结构的改变，还需要改变某些函数，有的函数是只支持Ipv4的，我们需要将这些函数改为即兼容的函数，根据官方的介绍，这些兼容函数主要是下面几个:WSAConnectByName : 可以直接通过主机名建立一个连接WSAConnectByList: 从一组主机名中建立一个连接getaddrinfo: 类似于gethostbyname, 但是gethostbyname只支持IPV4所以一般用这个函数来代替GetAdaptersAddresses: 这个函数用来代替原来的GetAdaptersInfoWSAConnectByName函数:函数原型如下:BOOL PASCAL WSAConnectByName( __in SOCKET s, __in LPSTR nodename, __in LPSTR servicename, __inout LPDWORD LocalAddressLength, __out LPSOCKADDR LocalAddress, __inout LPDWORD RemoteAddressLength, __out LPSOCKADDR RemoteAddress, __in const struct timeval* timeout, LPWSAOVERLAPPED Reserved ); s: 该参数为一个新创建的未绑定，未与其他主机建立连接的SOCKET，后续会采用这个socket来进行收发包的操作nodename: 主机名，或者主机的IP地址的字符串servicename: 服务名称，也可以是对应的端口号的字符串，传入服务名时需要传入那些知名的服务，比如HTTP、FTP等等， 其实这个字段本身就是需要传入端口的，传入服务名，最后函数会根据服务名称转化为这些服务的默认端口LocalAddressLength, LocalAddress， 返回当前地址结构，与长度RemoteAddressLength, RemoteAddress，返回远程主机的地址结构，与长度timeout: 超时值Reserved: 重叠IO结构为了使函数能够支持Ipv6，需要在调用前使用setsockopt函数对socket做相关设置，设置的代码如下:iResult = setsockopt(ConnSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) );调用函数的例子如下(该实例为微软官方的例子):SOCKET OpenAndConnect(LPWSTR NodeName, LPWSTR PortName) { SOCKET ConnSocket; DWORD ipv6only = 0; int iResult; BOOL bSuccess; SOCKADDR_STORAGE LocalAddr = {0}; SOCKADDR_STORAGE RemoteAddr = {0}; DWORD dwLocalAddr = sizeof(LocalAddr); DWORD dwRemoteAddr = sizeof(RemoteAddr); ConnSocket = socket(AF_INET6, SOCK_STREAM, 0); if (Co…

WinSock2 API
type: item
date: 2018-07-21T16:44:00+08:00
description: WinSock中提供的5种网络模型已经可以做到很高效了，特别是完成端口，它的高效的原因在于它不仅另外开启了线程来处理完成通知而不是占用主程序的时间，同时也在于我们在完成端口中运用了大量异步IO处理函数。比如WSASend、WSARecv等等。为了高效的处理网络IO，WinSock提供了大量这样的异步函数。这篇博文主要探讨这些函数的用法和他们与传统的巴克利套接字相比更加高效的秘密AcceptEx其实在使用TCP协议编程时，接受连接的过程也是需要进行收发包操作的，具体的过程请参考TCP的三次握手。针对这种特性WinSock提供了对应的异步操作函数AcceptEx。函数原型如下:BOOL AcceptEx( SOCKET sListenSocket, SOCKET sAcceptSocket, PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped );sListenSocket: 监听套接字sAcceptSocket：该参数是一个SOCKET的句柄，一旦连接成功建立，那么会使用该SOCKET作为通信的SOCKETlpOutputBuffer：是三个数据一体化的缓冲区的指针，这三个数据分别是接收连接时顺带接收客户端发过来的数据的缓冲，之后是本地地址结构的缓冲，最后是远程客户端地址结构的指针dwReceiveDataLength：是lpOutputBuffer的缓冲长度dwLocalAddressLength：是本地地址结构长度,其值等于sizeof(SOCKADDR)+16dwRemoteAddressLength：是远程客户端地址结构长度,其值也等于sizeof(SOCKADDR)+16lpdwBytesReceived：该参数用于返回接受连接请求时接收的数据的长度lpOverlapped：就是重叠I/O需要的结构第一个参数是一个十分重要的参数，这个参数是AcceptEx比较高效的一个重要的原因。从功能上来看它与传统的accept函数并没有什么区别，都是接受客户端连接的。它与accpet相比比较高效的原因如下：从内部机理来说accpet在内部其实有一个创建SOCKET的操作，当函数成功后会返回这个SOCKET，所以AcceptEx与accept相比少了一个创建SOCKET的操作，它的功能更加纯粹，这就给了我们一个启示：我们可以在初始化的时候创建大量的SOCKET，并投递到AcceptEx中，这样在接受连接时省去了创建SOCKET的时间，能够更快速的响应客户端的连接。由于AcceptEx不用创建SOCKET，所以它也将accept内部对socket设置的操作给省去了，也少了一些其他的附带操作，比如地址的解析，其实这里我们可以简单的理解为lpOutputBuffer中保存的信息就是TCP三次握手中的SYN包和ACK包，这些包的信息需要在函数返回后由用户通过其他方法来解析，而accpet帮我们解析了，所以AcceptEx比accept更加高效因为AcceptEx的设计目标纯粹就是为了性能,所以监听套接字的属性不会被代表客户端通讯的套接字自动继承。要继承属性(包括socket内部接受/发送缓存大小等等)就必须调用setsockopt使用选项SO_UPDATE_ACCEPT_CONTEXT,如下:int nRet = ::setsockopt(skClient, SOL_SOCKET ,SO_UPDATE_ACCEPT_CONTEXT ,(char *)&skListen, sizeof(skListen));AcceptEx函数除了能够接受客户端的连接之外，它也可以在接受连接的同时接收客户端随着连接请求一块发过来的数据，只要我们设置dwReceiveDataLength 参数大于0，并在lpOutputBuffer中分配相应的缓冲即可，但是这里会存在一个安全问题，当我们设置了这些之后，如果客户端只发送连接请求，但是不发送数据，AcceptEx会一直等待，如果有大量这样的客户端，那么可能会给服务器造成大量的资源浪费从而不能及时的服务其他正常客户端。要防止这样的情况，可以采用下列措施：设置dwReceiveDataLength为0，并且不分配对应的缓冲，也就是关闭这个接收数据的功能。启动一个监视线程对用于连接的SOCKET轮询调用：int iSecs; int iBytes = sizeof( int ); getsockopt( hAcceptSocket, SOL_SOCKET, …

WinSock 完成端口模型
type: item
date: 2018-07-06T16:50:00+08:00
description: title: WinSock 完成端口模型tags: [WinSock 模型, 网络编程, 完成端口]date: 2018-07-06 20:44:39categories: Windows 网络编程keywords: WinSock 模型, 网络编程, 完成端口之前写了关于Winsock的重叠IO模型，按理来说重叠IO模型与之前的模型相比，它的socket即是非阻塞的，也是异步的，它基本上性能非常高，但是它主要的缺点在于，即使我们使用历程来处理完成通知，但是我们知道历程它本身是在对应线程暂停，它借用当前线程的线程环境来执行完成通知，也就是说要执行完成通知就必须暂停当前线程的工作。这对工作线程来说也是一个不必要的性能浪费，这样我们自然就会想到，另外开辟一个线程来执行完成通知，而本来的线程就不需要暂停，而是一直执行它自身的任务。处于这个思想，WinSock提供了一个新的模型——完成端口模型。完成端口简介完成端口本质上是一个线程池的模型，它需要我们创建对应的线程放在那，当完成通知到来时，他会直接执行线程。在这5中模型中它的性能是最高的。在文件中我们也提到过完成端口，其实我们利用Linux上一切皆文件的思想来考虑这个问题就可以很方便的理解，既然我们需要异步的方式来读写网卡的信息，这与读写文件的方式类似，既然文件中存在完成端口模型，网络上存在也就不足为奇了。对于完成端口Windows没有引入新的API函数，而是仍然采用文件中一堆相关的函数。可以使用CreateIoCompletionPort来创建完成端口的句柄，该函数原型如下:HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, __in_opt HANDLE ExistingCompletionPort, __in ULONG_PTR CompletionKey, __in DWORD NumberOfConcurrentThreads );第一个参数是与完成端口绑定的文件句柄，如果我们要创建完成端口句柄，这个值必须传入INVALID_HANDLE_VALUE。如果是要将文件句柄与完成端口绑定，这个参数必须穿入一个支持完成端口的文件句柄。在Winsock中如果要绑定SOCKET到完成端口只需要将SOCKET强转为HANDLE。第二个参数是一个已知的完成端口句柄，如果是创建完成端口，这个参数填入NULL。第三个参数是一个LONG型的指针，它作为一个标志，由完成通知传入完成线程中，用来标识不同的完成通知。一般我们会定义一个扩展来OVERLAPPED结构来标识不同的完成通知，所以这个参数一般不用传入NULL。第四个参数是同时执行的线程数，如果是绑定文件句柄到完成端口，则这个参数填入0我们可以在对应的完成线程中调用GetQueuedCompletionStatus函数来获取完成通知，这个函数只有当有IO操作完成时才会返回，函数原型如下:BOOL WINAPI GetQueuedCompletionStatus( __in HANDLE CompletionPort, __out LPDWORD lpNumberOfBytes, __out PULONG_PTR lpCompletionKey, __out LPOVERLAPPED* lpOverlapped, __in DWORD dwMilliseconds ); 它的第一个参数是一个完成端口的句柄。第二个参数表示当前有多少字节的数据完成IO操作。第三个参数是一个标记值，用来标识不同文件句柄对应的完成通知，它是通过 CreateIoCompletionPort 函数设置的那个标识。第四个参数是OVERLAPPED结构。第五个参数表示等待的时间，如果填入INFINITE则会一直等到有IO操作完成。完成端口的示例：下面是一个完成端口的示例typedef struct _tag_MY_OVERLAPPED { OVERLAPPED m_overlapped; SOCKET m_sClient; long m_lEvent; DWORD m_dwNumberOfBytesRecv; DWORD m_dwFlags; char *m_pszBuf; LONG m_dwBufSize; }MY_OVERLAPPED, *LPMY_OVERLAPPED; unsigned int __stdcall IOCPThread(LPVOID lpParameter); #define BUFFER_SIZE 1024 #define SERVER_PORT 6000 int _tmain(int argc, TCHAR *argv) { WSADATA wd = {0}; WSAStar…

WinSock WSAEventSelect 模型
type: item
date: 2018-06-23T18:13:00+08:00
description: 在前面我们说了WSAAsyncSelect 模型，它相比于select模型来说提供了这样一种机制：当发生对应的IO通知时会立即通知操作系统，并调用对应的处理函数，它解决了调用send和 recv的时机问题，但是它有一个明显的缺点，就是它必须依赖窗口。对此WinSock 提供了另一种模型 WSAEventSelect模型简介该模型主要特色在于它使用事件句柄来完成SOCKET事件的通知。与WSAAsyncSelect 模型类似，它也允许使用事件对象来完成多个socket的完成通知。该模型首先在每个socket句柄上调用WSACreateEvent来创建一个WSAEvent对象句柄（早期的WSAEvent与传统的Event句柄有一定的区别，但是从WinSock2.0 以后二者是同一个东西）。接着调用WSAEventSelect将SOCKET句柄和WSAEvent对象绑定，最终通过WSAWaitForMultiEvents来等待WSAEvent变为有信号，然后再来处理对应的socketWSAEvent有两种工作模式和工作状态工作状态有有信号和无信号两种工作模式有手工重置和人工重置，手工重置指的是每当WSAWaitForMultiEvents或者WSAWaitForSingleEvents 返回之后，WSAEvent不会自动变为无信号，需要手工调用WSAResetEvent来将WSAEvent对象设置为无信号，而自动重置表示每次等待函数返回后会自动重置为无信号；调用WSACreateEvent创建的WSAEvent对象是需要手工重置的，如果想创建自动重置的WSAEvent对象可以调用CreateEvent函数来创建（由于WinSock2.0 之后二者没有任何区别，所以只需要调用CreateEvent并将返回值强转为WSAEvent即可）WSAEventSelect函数的原型如下:int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);其中s表示对应的SOCKET，hEventObject表示对应的WSAEvent对象，lNetworkEvents 表示我们需要处理哪些事件，它有一些对应的宏定义网络事件对应的含义FD_READ当前可以进行数据接收操作，此时可以调用像 recv, recvfrom, WSARecv, 或者 WSARecvFrom 这样的函数FD_WRITE此时可以发送数据，可以调用 send, sendto, WSASend, or WSASendToFD_ACCEPT可以调用accept (Windows Sockets) 或者 WSAAccept 除非返回的错误代码是WSATRY_AGAIN.FD_CONNECT表示当前可以连接远程服务器FD_CLOSE当前收到关闭的消息当WSAWaitForMultipleEvents返回时同时会返回一个序号，用于标识是数组中的哪个WSAEvent有信号，我们使用 index - WSA_WAIT_EVENT_0 来获取对应WSAEvent在数组中的下标，然后根据这个事件对象找到对应的SOCKET即可获得了对应的SOCKET以后，还需要获取到当前是哪个事件发生导致它变为有信号，我们可以调用WSAEnumNetworkEvents函数来获取对应发生的网络事件int WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );s就是要获取其具体事件通知的SOCKET句柄hEventObject就是对应的WSAEvent句柄,可以不传入,因为SOCKET句柄已经说明了要获取那个句柄上的通知,当然如果传入了,那么这个函数会对这个WSAEvent做一次重置,置为无信号的状态,相当于WSAResetEvent调用。此时我们就不需要调用WSAResetEvent函数了最后一个参数是一个结构，结构的定义如下:typedef struct _WSANETWORKEVENTS { long lNetworkEvents; int iErrorCode[FD_MAX_EVENTS]; } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;第一个数据是当前产生的网络事件。iErrorCode数组是对应每个网络事件可能发生的错误代码,对于每个事件错误代码其具体数组下标是预定义的一组FD_开头的串再加上一个_BIT结尾的宏,比如FD_READ事件对应的错误码下标是FD_READ_BIT下面的代码演示了处理接收(读取)数据的事件错误的例子代码if (NetworkEvents…

select模型
type: item
date: 2015-09-15T10:35:00+08:00
description: 在Windows中所有的socket函数都是阻塞类型的，也就是说只有网络中有特定的事件发生时才会返回，在没有发生事件时会一直等待，虽说我们将它们设置为非阻塞状态，但是在对于服务器段而言，肯定会一直等待客户端的消息，也就是说即使设置为非阻塞状态，时间到了函数返回，但是程序不能结束，需要一个循环不断的侦听，特别是对于有多个客户端需要管理的时候，每一个与客户端通信的socket都需要一个侦听，这样管理起来非常麻烦，我们希望系统帮助我们管理，告诉我们有哪些socket现在可以操作。为了实现这个，我们可以使用select模型select模型中需要一个结构体fd_set，该结构体是一个socket的集合，我们可以看到该结构体的定义：typedef struct fd_set { u_int fd_count; /* how many are SET? */ SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ } fd_set;从这个定义中可以看到，结构体中主要保存了一个socket的数组和一个保存数组的大小变量；使用select模型主要使用函数select，该函数原型如下：int select ( int nfds, //系统保留，无意义 fd_set FAR * readfds,//可读的socket集合 fd_set FAR * writefds,//可写的socket集合 fd_set FAR * exceptfds,//外带socket集合 const struct timeval FAR * timeout//该函数的超时值 );在程序中使用该函数前需要在特定的集合中放入需要检测的socket值，当发生某一时间导致该函数返回时，函数会将特定集合中未待决的socket全部剔除出去，保留待决套接字，比如在readfds集合中放入几个套接字并执行完成函数，那么留下的套接字都是可以从系统的相应缓冲区读数据的。通过遍历相应的集合我们知道如何对套接字做相应的操作；select模型最多支持64个套接字，这个值由FD_SETSIZE宏定义的，我们可以修改这个宏的值，以便支持更多的套接字，修改时尽量不要在系统文件中修改，在我们的工程文件中修改，可用使用如下方式：#ifdef FD_SETSIZE #undef FD_SETSIZE #endif #define FD_SETSIZE 200这段代码使得select模型支持200个套接字；虽然可以修改，但是这个数组太大，会消耗过多的系统资源，每次在遍历数组时总会从头到尾遍历，数组太大效率必然底下，所以最好不要修改这个值，处理大于64个套接字的情况下可以使用多线程的方式，多定义几个集合处理;为了操作这个集合，Windows专门定义了一组宏，他们分别是：FD_SET(fd, &set) //将fd套接字压入集合set中 FD_ISSET(fd, &set)//判断fd是否在set中 FD_ZERO(&set)//将集合set清零 FD_CLR(fd, &set)//将fd从集合set中删除下面说一下服务端一个简单的select模型的编写1)创建套接字，绑定、侦听；2）等待客户端链接3)将连接返回的套接字压入一个数组中保存4)将数组的套接字填入集合中5)调用select函数6)检测特定集合中的套接字7)进行读写操作8)返回到第四步，等待客户端下一步请求在编写时需要注意以下几点：1）为了与多个客户端保持连接，需要一个数组保存与客户端连接的所有的socket，由于select函数只会执行一次，每次返回后需要再次将徐监控的套接字压入集合，调用select，以便进行下一次检测；所以一般将这一步写在一个死循环中2）注意select是一个阻塞函数，所以为了可以支持多个客户端可以采用一些方法：第一种就是采用多线程的方式，每有一个客户端连接都需要将新开一个线程处理并调用select监控；另一种就是调用select对侦听套接字以及与客户端通信的套接字；为什么可以这样呢，这就要说到TCP/IP中的三次握手，首先一般由客户端发起链接，发送一条数据包到服务器，服务器接收到数据，发送一条确认信息给客户端，然后客户端再发送一条数据，这样就正式建立连接，所以在客户端与服务器建立连接时必然会发送数据，而服务器一定会收到数据，所以将侦听套接字放入到read集合中，当有客户端需要连接时自然会收到一条数据，这个时候select会返回，我们需要校验集合中的套接字是否是侦听套接字，如果是则表明有客户端需要连接；这样当客户端有请求select会返回，可以进行下一次的侦听，没有请求，会死锁在select函数上，但是对于所有客户端并没有太大的影响；3）我们用数组存储所有的套接字时，每当有客户端链…

socket模型处理多个客户端
type: item
date: 2015-09-12T10:37:00+08:00
description: 最近学完了简单的socket编程，发现其实socket的网络编程其实并没有什么难度，只是简单的函数调用，记住客户端与服务端的步骤，写起来基本没有什么问题。在服务器程序的设计中，一个服务器不可能只相应一个客户端的链接，为了响应多个客户端的链接，需要使用多线程的方式，每当有一个客户端连接进来，我们就开辟一个线程，用来处理双方的交互（主要是利用recv或者recvfrom用于收发信息），由于但是在网络中可能出现这样一种情况：由于处理比较复杂，下一条信息到来之后，上一条信息的处理还没有完成，这样信息太多了之后系统的缓冲占满之后可能会发生丢包的现象，所以为了解决这个问题，需要另外再开一个线程，专门用来处理接收到的数据，这样总共至少有3个线程，主线程，收发信息的线程，处理线程；这样可能也不完整，处理的操作种类多了的话可能需要根据不同的请求来开辟不同的线程用来处理这一类请求，下面是实现这一思路的部分代码：全局变量：DWORD WINAPI AcceptThread(LPVOID lpParameter); DWORD WINAPI RecvThread(LPVOID lpParameter); DWORD g_nAcceptID = 123; DWORD g_nRecvID = 234; HANDLE g_hAccpetThread; HANDLE g_hRecvThread;主线程函数：int _tmain(int argc, _TCHAR* argv[]) { WSADATA wd; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (INVALID_SOCKET == sockListen) { cout << "创建侦听套接字失败，错误码为：" << WSAGetLastError() << endl; return -1; } SOCKADDR_IN srvAddr = { 0 }; srvAddr.sin_family = AF_INET; srvAddr.sin_port = htons(6666); srvAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (SOCKET_ERROR == bind(sockListen, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR))) { cout << "绑定失败，错误码为:" << WSAGetLastError() << endl; WSACleanup(); return -1; } if (SOCKET_ERROR == listen(sockListen, 5)) { cout << "侦听失败，错误码为：" << WSAGetLastError() << endl; WSACleanup(); return -1; } while (true) { SOCKET sockConn = accept(sockListen, NULL, 0); if (INVALID_SOCKET == sockConn) { cout << "本次连接失败，即将进入下一次连接，错误码为:" << WSAGetLastError() << endl; closesocket(sockConn); closesocket(sockListen); WSACleanup(); continue; } g_hAccpetThread = CreateThread(NULL, 0, AcceptThread, &sockConn, 0, &g_nAcceptID); } WaitForSingleObject(g_hAccpetThread, INFINITE); WSACleanup(); return 0; }收发数据函数：DWORD WINAPI AcceptThread(LPVOID lpParameter) { cout << "有客户端连接进来" << endl; SOCKET sockConn = *(SOCKET*)lpParameter; while (true) { char *pszBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY | HEAP_NO_SERIALIZE, 255); if (SOCKET_ERROR == recv(sockConn, pszBuf, 255, 0)) { cout << "接受数据失败，错误码为:" << WSAGetLastError() << endl; …