windows socket编程

 来自:

https://cloud.tencent.com/developer/article/2105302

一、什么是Socket

socket即套接字,用于描述地址和端口,是一个通信链的句柄。应用程序通过socket向网络发出请求或者回应。

sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);前两种较常用。基于TCP的socket编程是采用的流式套接字。

(1)SOCK_STREAM表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常用的HTTP协议就使用SOCK_STREAM传输数据,因为要确保数据的正确性,否则网页不能正常解析。

(2)SOCK_DGRAM表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为SOCK_DGRAM所做的校验工作少,所以效率比SOCK_STREAM高。

QQ视频聊天和语音聊天就使用SOCK_DGRAM传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。

注意:SOCK_DGRAM没有想象中的糟糕,不会频繁的丢失数据,数据错读只是小概率事件。

有可能多种协议使用同一种数据传输方式,所以在socket编程中,需要同时指明数据传输方式和协议。

二、客户端/服务端模式:

在TCP/IP网络应用中,通信的两个进程相互作用的主要模式是客户/服务器模式,即客户端向服务器发出请求,服务器接收请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:

(1)建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而就让拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。

(2)网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区。

因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务端模式的TCP/IP。

服务端:建立socket,声明自身的端口号和地址并绑定到socket,使用listen打开监听,然后不断用accept去查看是否有连接,如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket,如果不再需要等待任何客户端连接,那么用closeSocket关闭掉自身的socket。

客户端:建立socket,通过端口号和地址确定目标服务器,使用Connect连接到服务器,send发送消息,等待处理,通信完成后调用closeSocket关闭socket。

三、编程步骤

(1)服务端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、绑定套接字到一个IP地址和一个端口上(bind());

3、将套接字设置为监听模式等待连接请求(listen());

4、请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());

5、用返回的套接字和客户端进行通信(send()/recv());

6、返回,等待另一个连接请求;

7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

(2)客户端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、向服务器发出连接请求(connect());

3、和服务器进行通信(send()/recv());

4、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

四、windows下实现socket简单实例

使用软件:devc++

(一)TCP协议

(1)代码

服务端:server.cpp

#include <stdio.h>  
#include <winsock2.h>  
  
#pragma comment(lib,"ws2_32.lib")  
  
int main(int argc, char* argv[])  
{  
    //初始化WSA  
    WORD sockVersion = MAKEWORD(2,2);  
    WSADATA wsaData;  
    if(WSAStartup(sockVersion, &wsaData)!=0)  
    {  
        return 0;  
    }  
  
    //创建套接字  
    SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
    if(slisten == INVALID_SOCKET)  
    {  
        printf("socket error !");  
        return 0;  
    }  
  
    //绑定IP和端口  
    sockaddr_in sin;  
    sin.sin_family = AF_INET;  
    sin.sin_port = htons(8888);  
    sin.sin_addr.S_un.S_addr = INADDR_ANY;   
    if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)  
    {  
        printf("bind error !");  
    }  
  
    //开始监听  
    if(listen(slisten, 5) == SOCKET_ERROR)  
    {  
        printf("listen error !");  
        return 0;  
    }  
  
    //循环接收数据  
    SOCKET sClient;  
    sockaddr_in remoteAddr;  
    int nAddrlen = sizeof(remoteAddr);  
    char revData[255];   
    while (true)  
    {  
        printf("等待连接...\n");  
        sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);  
        if(sClient == INVALID_SOCKET)  
        {  
            printf("accept error !");  
            continue;  
        }  
        printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));  
          
        //接收数据  
        int ret = recv(sClient, revData, 255, 0);         
        if(ret > 0)  
        {  
            revData[ret] = 0x00;  
            printf(revData);  
        }  
  
        //发送数据  
        const char * sendData = "你好,TCP客户端!\n";  
        send(sClient, sendData, strlen(sendData), 0);  
        closesocket(sClient);  
    }  
      
    closesocket(slisten);  
    WSACleanup();  
    return 0;  
} 

客户端代码:client.cpp

#include<WINSOCK2.H>
#include<STDIO.H>
#include<iostream>
#include<cstring>
using namespace std;
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA data;
	if(WSAStartup(sockVersion, &data)!=0)
	{
		return 0;
	}
	while(true){
		SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if(sclient == INVALID_SOCKET)
		{
			printf("invalid socket!");
			return 0;
		}
		
		sockaddr_in serAddr;
		serAddr.sin_family = AF_INET;
		serAddr.sin_port = htons(8888);
		serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
		if(connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
		{  //连接失败 
			printf("connect error !");
			closesocket(sclient);
			return 0;
		}
		
		string data;
		cin>>data;
		const char * sendData;
		sendData = data.c_str();   //string转const char* 
		//char * sendData = "你好,TCP服务端,我是客户端\n";
		send(sclient, sendData, strlen(sendData), 0);
		//send()用来将数据由指定的socket传给对方主机
		//int send(int s, const void * msg, int len, unsigned int flags)
		//s为已建立好连接的socket,msg指向数据内容,len则为数据长度,参数flags一般设0
		//成功则返回实际传送出去的字符数,失败返回-1,错误原因存于error 
		
		char recData[255];
		int ret = recv(sclient, recData, 255, 0);
		if(ret>0){
			recData[ret] = 0x00;
			printf(recData);
		} 
		closesocket(sclient);
	}
	
	
	WSACleanup();
	return 0;
	
}

(2)可能遇到的问题

(1)undefined reference to ‘_imp_WSAStartup’

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

解决方案:

工具->编译选项->在连接器命令行加入如下命令里面添加-lwsock32 即可

然后重启devc++运行程序问题解决。

(2)deprecated conversion from string constant to ‘char *'[-Wwrite-strings]

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

解决方法:将char * 改为const char *

(3)结果运行

先运行服务端,运行service.cpp,服务端显示如下:

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

然后运行客户端,运行client.cpp,在客户端输入数据,即可传送到服务器端显示如下:

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

(4)部分代码说明

第一步:加载/释放Winsock库:

加载方法:

WORD sockVersion = MAKEWORD(2,2);
WSADATA wsaData;
//初始化socket资源
if(WSAStartup(sockVersion, &wsaData)!=0)
{
   return 0;  //代表失败
}

释放方法:

WSACleanup();

第二步:构造SOCKET

1. 服务端:构造监听SOCKET,流式SOCKET

//创建套接字
    SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(slisten == INVALID_SOCKET)
    {
        printf("socket error !");
        return 0;
    }

2. 客户端:构造通讯SOCKET,流式SOCKET

//创建套接字
   SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
   if(sclient == INVALID_SOCKET)
    {
       printf("invalid socket !");
       return 0;
    }

第三步:配置监听地址和端口,服务端绑定IP地址和端口,客户端连接目的IP地址和端口:

1. 服务端:

//绑定IP和端口
   sockaddr_in sin;
   sin.sin_family = AF_INET;
   sin.sin_port = htons(8888);   //本地监听端口:8888
   sin.sin_addr.S_un.S_addr = INADDR_ANY;
   if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) ==SOCKET_ERROR)  //尝试绑定
    {
       printf("bind error !");
    }
//绑定成功后就开始监听
   if(listen(slisten, 5) == SOCKET_ERROR)
    {
       printf("listen error !");
       return 0;
    }

2. 客户端:

//配置要连接的地址和端口   
   sockaddr_in serAddr;
   serAddr.sin_family = AF_INET;
   serAddr.sin_port = htons(8888);
   serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
   if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) ==SOCKET_ERROR)  //尝试连接
    {
       printf("connect error !");
       closesocket(sclient);
       return 0;
    }

第四步:服务端/客户端连接

1. 服务端:等待客户端接入

SOCKET Command_Sock = accept(Listen_Sock,…)

2. 客户端:请求与服务端连接

int ret = connect(Client_Sock, …)

第五步:收/发数据

1. 服务端:等待客户接入 charbuf[1024].

接收数据:recv(Command_Sock, buf, …)

发送数据:send(Command_Sock, buf, …)

2. 客户端:请求与服务端连接char buf[1024].

发送数据:send(Client_Sock, buf, …)

接收数据:recv(Client_Sock, buf, …)

第六步:关闭SOCKET

1. 服务端关闭SOCKET

closesocket(Listen_Sock)

closesocket(Command_Sock)

2. 客户端关闭SOCKET

closesocket(Client_Sock)

(二)UDP协议

服务端代码:

#include <stdio.h> 
#include <winsock2.h> 
 
#pragma comment(lib,"ws2_32.lib")  
 
int main(int argc, char* argv[]) 
{ 
   WSADATA wsaData; 
   WORD sockVersion = MAKEWORD(2,2); 
   if(WSAStartup(sockVersion, &wsaData) != 0) 
   { 
       return 0; 
   } 
 
   SOCKET serSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  
   if(serSocket == INVALID_SOCKET) 
   { 
       printf("socket error !"); 
       return 0; 
   } 
 
   sockaddr_in serAddr; 
   serAddr.sin_family = AF_INET; 
   serAddr.sin_port = htons(8888); 
   serAddr.sin_addr.S_un.S_addr = INADDR_ANY; 
   if(bind(serSocket, (sockaddr *)&serAddr, sizeof(serAddr)) ==SOCKET_ERROR) 
   { 
       printf("bind error !"); 
       closesocket(serSocket); 
       return 0; 
   } 
     
   sockaddr_in remoteAddr; 
   int nAddrLen = sizeof(remoteAddr);  
   while (true) 
   { 
       char recvData[255];   
       int ret = recvfrom(serSocket, recvData, 255, 0, (sockaddr*)&remoteAddr, &nAddrLen); 
       if (ret > 0) 
       { 
           recvData[ret] = 0x00; 
           printf("接受到一个连接:%s \r\n",inet_ntoa(remoteAddr.sin_addr)); 
           printf(recvData);            
       } 
 
       const char * sendData = "一个来自服务端的UDP数据包\n"; 
       sendto(serSocket, sendData,strlen(sendData), 0, (sockaddr *)&remoteAddr, nAddrLen);     
 
   } 
   closesocket(serSocket);  
   WSACleanup(); 
   return 0; 
} 

客户端代码:

#include <stdio.h> 
#include <winsock2.h> 
 
#pragma comment(lib,"ws2_32.lib")  
 
int main(int argc, char* argv[]) 
{ 
   WORD socketVersion = MAKEWORD(2,2); 
   WSADATA wsaData;  
   if(WSAStartup(socketVersion, &wsaData) != 0) 
   { 
       return 0; 
   } 
   SOCKET sclient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); 
     
   sockaddr_in sin; 
   sin.sin_family = AF_INET; 
   sin.sin_port = htons(8888); 
   sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); 
   int len = sizeof(sin); 
     
   const char * sendData = "来自客户端的数据包.\n"; 
   sendto(sclient, sendData, strlen(sendData), 0, (sockaddr *)&sin,len); 
 
   char recvData[255];      
   int ret = recvfrom(sclient, recvData, 255, 0, (sockaddr *)&sin,&len); 
   if(ret > 0) 
   { 
       recvData[ret] = 0x00; 
       printf(recvData); 
   } 
 
   closesocket(sclient); 
   WSACleanup(); 
   return 0; 
} 

结果显示如下:

服务端:

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

客户端:

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

五、Windows下的socket程序和Linux思路相同,细节处区别如下:

(1)Windows下的socket程序依赖Winsock.dll或ws2_32.dll,必须提前加载。DLL有两种加载方式。

(2)Linux使用“文件描述符”的概念,而Windows使用“文件句柄”的概念;Linux不区分socket文件和普通文件,而Windows区分;Linux下socket()函数的返回值为int类型,而Windows下为SOCKET类型,也就是句柄。

(3)Linux下使用read()/write()函数读写,而Windows下使用recv()/send()函数发送和接收

(4)关闭socket时,Linux使用close()函数,而Windows使用closesocket()函数。

 

其中 出现C4996 错误:

方法一:#define WARNING 

 

参考:

Windows下Winsock.dll与ws2_32.dll 区别:

在Windows中,Winsock.dll(Windows Socket DLL)和 ws2_32.dll(Winsock 2 DLL)是两个不同的 DLL(动态链接库),但它们都与网络编程和套接字操作有关。

  1. Winsock.dll:

    • Legacy Winsock Library: Winsock.dll是Windows原始的套接字库,用于支持最初的Winsock标准(Winsock 1.1及其之前的版本)。
    • 功能有限: 由于是早期的实现,其功能相对有限,缺少一些现代网络编程的特性和改进。
    • 不建议使用: 对于新的应用程序,Microsoft通常建议使用Winsock 2库而不是Winsock.dll,因为后者已经过时。
  2. ws2_32.dll:

    • Winsock 2 Library: ws2_32.dll是Winsock 2库的实现,提供了对更多网络协议和功能的支持。
    • 更现代: Winsock 2引入了一些重要的改进,包括支持IPv6、更好的异步套接字操作、更多协议的支持等。
    • 推荐使用: 对于现代应用程序,尤其是需要利用新特性和更好性能的应用程序,推荐使用ws2_32.dll。

综上所述,通常情况下,开发人员应该使用 ws2_32.dll,因为它提供了更先进、更强大的网络编程功能。Winsock.dll主要是为了向后兼容而存在,对于新的应用程序来说,使用Winsock 2是更好的选择。

一、什么是Socket

socket即套接字,用于描述地址和端口,是一个通信链的句柄。应用程序通过socket向网络发出请求或者回应。

sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);前两种较常用。基于TCP的socket编程是采用的流式套接字。

(1)SOCK_STREAM表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常用的HTTP协议就使用SOCK_STREAM传输数据,因为要确保数据的正确性,否则网页不能正常解析。

(2)SOCK_DGRAM表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为SOCK_DGRAM所做的校验工作少,所以效率比SOCK_STREAM高。

QQ视频聊天和语音聊天就使用SOCK_DGRAM传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。

注意:SOCK_DGRAM没有想象中的糟糕,不会频繁的丢失数据,数据错读只是小概率事件。

有可能多种协议使用同一种数据传输方式,所以在socket编程中,需要同时指明数据传输方式和协议。

二、客户端/服务端模式:

在TCP/IP网络应用中,通信的两个进程相互作用的主要模式是客户/服务器模式,即客户端向服务器发出请求,服务器接收请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:

(1)建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而就让拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。

(2)网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区。

因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务端模式的TCP/IP。

服务端:建立socket,声明自身的端口号和地址并绑定到socket,使用listen打开监听,然后不断用accept去查看是否有连接,如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket,如果不再需要等待任何客户端连接,那么用closeSocket关闭掉自身的socket。

客户端:建立socket,通过端口号和地址确定目标服务器,使用Connect连接到服务器,send发送消息,等待处理,通信完成后调用closeSocket关闭socket。

三、编程步骤

(1)服务端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、绑定套接字到一个IP地址和一个端口上(bind());

3、将套接字设置为监听模式等待连接请求(listen());

4、请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());

5、用返回的套接字和客户端进行通信(send()/recv());

6、返回,等待另一个连接请求;

7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

(2)客户端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、向服务器发出连接请求(connect());

3、和服务器进行通信(send()/recv());

4、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

四、windows下实现socket简单实例

使用软件:devc++

(一)TCP协议

(1)代码

服务端:server.cpp

#include <stdio.h>  
#include <winsock2.h>  
  
#pragma comment(lib,"ws2_32.lib")  
  
int main(int argc, char* argv[])  
{  
    //初始化WSA  
    WORD sockVersion = MAKEWORD(2,2);  
    WSADATA wsaData;  
    if(WSAStartup(sockVersion, &wsaData)!=0)  
    {  
        return 0;  
    }  
  
    //创建套接字  
    SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
    if(slisten == INVALID_SOCKET)  
    {  
        printf("socket error !");  
        return 0;  
    }  
  
    //绑定IP和端口  
    sockaddr_in sin;  
    sin.sin_family = AF_INET;  
    sin.sin_port = htons(8888);  
    sin.sin_addr.S_un.S_addr = INADDR_ANY;   
    if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)  
    {  
        printf("bind error !");  
    }  
  
    //开始监听  
    if(listen(slisten, 5) == SOCKET_ERROR)  
    {  
        printf("listen error !");  
        return 0;  
    }  
  
    //循环接收数据  
    SOCKET sClient;  
    sockaddr_in remoteAddr;  
    int nAddrlen = sizeof(remoteAddr);  
    char revData[255];   
    while (true)  
    {  
        printf("等待连接...\n");  
        sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);  
        if(sClient == INVALID_SOCKET)  
        {  
            printf("accept error !");  
            continue;  
        }  
        printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));  
          
        //接收数据  
        int ret = recv(sClient, revData, 255, 0);         
        if(ret > 0)  
        {  
            revData[ret] = 0x00;  
            printf(revData);  
        }  
  
        //发送数据  
        const char * sendData = "你好,TCP客户端!\n";  
        send(sClient, sendData, strlen(sendData), 0);  
        closesocket(sClient);  
    }  
      
    closesocket(slisten);  
    WSACleanup();  
    return 0;  
} 

客户端代码:client.cpp

#include<WINSOCK2.H>
#include<STDIO.H>
#include<iostream>
#include<cstring>
using namespace std;
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA data;
	if(WSAStartup(sockVersion, &data)!=0)
	{
		return 0;
	}
	while(true){
		SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if(sclient == INVALID_SOCKET)
		{
			printf("invalid socket!");
			return 0;
		}
		
		sockaddr_in serAddr;
		serAddr.sin_family = AF_INET;
		serAddr.sin_port = htons(8888);
		serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
		if(connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
		{  //连接失败 
			printf("connect error !");
			closesocket(sclient);
			return 0;
		}
		
		string data;
		cin>>data;
		const char * sendData;
		sendData = data.c_str();   //string转const char* 
		//char * sendData = "你好,TCP服务端,我是客户端\n";
		send(sclient, sendData, strlen(sendData), 0);
		//send()用来将数据由指定的socket传给对方主机
		//int send(int s, const void * msg, int len, unsigned int flags)
		//s为已建立好连接的socket,msg指向数据内容,len则为数据长度,参数flags一般设0
		//成功则返回实际传送出去的字符数,失败返回-1,错误原因存于error 
		
		char recData[255];
		int ret = recv(sclient, recData, 255, 0);
		if(ret>0){
			recData[ret] = 0x00;
			printf(recData);
		} 
		closesocket(sclient);
	}
	
	
	WSACleanup();
	return 0;
	
}

(2)可能遇到的问题

(1)undefined reference to ‘_imp_WSAStartup’

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

解决方案:

工具->编译选项->在连接器命令行加入如下命令里面添加-lwsock32 即可

然后重启devc++运行程序问题解决。

(2)deprecated conversion from string constant to ‘char *'[-Wwrite-strings]

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

解决方法:将char * 改为const char *

(3)结果运行

先运行服务端,运行service.cpp,服务端显示如下:

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

然后运行客户端,运行client.cpp,在客户端输入数据,即可传送到服务器端显示如下:

windows环境下用c++实现socket编程
windows环境下用c++实现socket编程

(4)部分代码说明

第一步:加载/释放Winsock库:

加载方法:

WORD sockVersion = MAKEWORD(2,2);
WSADATA wsaData;
//初始化socket资源
if(WSAStartup(sockVersion, &wsaData)!=0)
{
   return 0;  //代表失败
}

释放方法:

WSACleanup();

第二步:构造SOCKET

1. 服务端:构造监听SOCKET,流式SOCKET

//创建套接字
    SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(slisten == INVALID_SOCKET)
    {
        printf("socket error !");
        return 0;
    }

2. 客户端:构造通讯SOCKET,流式SOCKET

//创建套接字
   SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
   if(sclient == INVALID_SOCKET)
    {
       printf("invalid socket !");
       return 0;
    }

第三步:配置监听地址和端口,服务端绑定IP地址和端口,客户端连接目的IP地址和端口:

1. 服务端:

//绑定IP和端口
   sockaddr_in sin;
   sin.sin_family = AF_INET;
   sin.sin_port = htons(8888);   //本地监听端口:8888
   sin.sin_addr.S_un.S_addr = INADDR_ANY;
   if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) ==SOCKET_ERROR)  //尝试绑定
    {
       printf("bind error !");
    }
//绑定成功后就开始监听
   if(listen(slisten, 5) == SOCKET_ERROR)
    {
       printf("listen error !");
       return 0;
    }

2. 客户端:

//配置要连接的地址和端口   
   sockaddr_in serAddr;
   serAddr.sin_family = AF_INET;
   serAddr.sin_port = htons(8888);
   serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
   if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) ==SOCKET_ERROR)  //尝试连接
    {
       printf("connect error !");
       closesocket(sclient);
       return 0;
    }

第四步:服务端/客户端连接

1. 服务端:等待客户端接入

SOCKET Command_Sock = accept(Listen_Sock,…)

2. 客户端:请求与服务端连接

int ret = connect(Client_Sock, …)

第五步:收/发数据

1. 服务端:等待客户接入 charbuf[1024].

接收数据:recv(Command_Sock, buf, …)

发送数据:send(Command_Sock, buf, …)

2. 客户端:请求与服务端连接char buf[1024].

发送数据:send(Client_Sock, buf, …)

接收数据:recv(Client_Sock, buf, …)

第六步:关闭SOCKET

1. 服务端关闭SOCKET

closesocket(Listen_Sock)

closesocket(Command_Sock)

2. 客户端关闭SOCKET

closesocket(Client_Sock)

 

 

 

 

目前关于 socket 通信的教程并不少,但是存在一个现象:贴了代码的文章,对于代码的注释不够详细,导致读者频繁的去搜索某个函数的参数构成、使用方法等,十分地麻烦。我在进行学习的时候,在程序中的注释写得实在是太过于密密麻麻了,索性写个更详细的笔记,对用到的知识点进行一个综合的整理吧,省得一下子开十几个网页。。。

本文的详解是基于windows环境下用c++实现socket编程这篇文章进行的。因此对于TCP/IP以及socket通信的基础知识就不在赘述了,本文着重于带你一行一行地对代码进行详细解释。具体的解释由网络搜集整理而成,感谢大佬们。

首先贴一个服务器端的完整代码:(注释写了一半,实在写不下去了,太多了)

/*****************************************************************************************************************************
*	1、加载套接字库,创建套接字(WSAStartup()/socket());
*	2、绑定套接字到一个IP地址和一个端口上(bind());
*	3、将套接字设置为监听模式等待连接请求;
*	4、请求到来之后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
*	5、用返回的套接字和客户端进行通信(send()/recv());
*	6、返回,等待另一个连接请求
*	7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
*****************************************************************************************************************************/
#include<iostream>
#include<WinSock2.h>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
 
int main()
{
	//初始化WSA
	WORD sockVersion=MAKEWORD(2,2);
	WSADATA wsaData;//WSADATA结构体变量的地址值
 
	//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
	//成功时会返回0,失败时返回非零的错误代码值
	if(WSAStartup(sockVersion,&wsaData)!=0)
	{
		cout<<"WSAStartup() error!"<<endl;
		return 0;
	}
 
	//创建套接字
	SOCKET slisten=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(slisten==INVALID_SOCKET)
	{
		cout<<"socket error !"<<endl;
		return 0;
	}
 
	//绑定IP和端口
	sockaddr_in sin;//ipv4的指定方法是使用struct sockaddr_in类型的变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8888);//设置端口。htons将主机的unsigned short int转换为网络字节顺序
	sin.sin_addr.S_un.S_addr = INADDR_ANY;//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址
	//bind函数把一个地址族中的特定地址赋给scket。
	if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf("bind error !");
	}
 
	//开始监听
	if(listen(slisten,5)==SOCKET_ERROR)
	{
		cout<<"listen error !"<<endl;
		return -1;
	}
 
	//循环接收数据
	SOCKET sclient;
	sockaddr_in remoteAddr;//sockaddr_in常用于socket定义和赋值,sockaddr用于函数参数
	int nAddrlen=sizeof(remoteAddr);
	char revData[255];
	while(true)
	{
		cout<<"等待连接。。。"<<endl;
		sclient=accept(slisten,(sockaddr *)&remoteAddr,&nAddrlen);
		if(sclient==INVALID_SOCKET)
		{
			cout<<"accept error !"<<endl;
			continue;
		}
		cout<<"接收到一个连接:"<<inet_ntoa(remoteAddr.sin_addr)<<endl;
		//接收数据
		int ret=recv(sclient,revData,255,0);
		if(ret>0)
		{
			revData[ret]=0x00;
			cout<<revData<<endl;
		}
		//发送数据
		const char * sendData = "你好,TCP客户端!\n";
		send(sclient, sendData, strlen(sendData), 0);
		closesocket(sclient);
	}
	closesocket(slisten);
	WSACleanup();
	system("pause");
	//return 0;
}

接下来开始按行解释:

#include<WinSock2.h>
使用 socket 通信必须包含对应的头文件 <WinSock2.h> 。在添加头文件的时候能看到自动补全中还存在一个 <WinSock.h> 的头文件,那么这两者有啥区别呢?
<WinSock2.h> 设计的目的是替代 <WinSock.h>,而不是扩展它。在 <WinSock.h> 中定义的所有内容在 <WinSock2.h> 中也都定义了.
#pragma comment(lib,"ws2_32.lib")

#pragma comment(lib,"Ws2_32.lib")表示链接 Ws2_32.lib 这个库。

这种方式和在工程设置_链接库里面添加 Ws2_32.lib 的效果一样,不过这种方法写的

程序,别人在使用你的代码的时候就不用再设置工程了。

int main()
{
	//初始化WSA
	WORD sockVersion=MAKEWORD(2,2);
    WSADATA wsaData;//WSADATA结构体变量的地址值
}

MAKEWORD 语法如下:

        WORD MAKEWORD(
                BYTE below;  //指定一个低位的新值
                BYTE high;  //指定一个高位的新值
        );

先将两个参数转换为二进制,然后将第一个参数放在低位,第二个参数放在高位,最后转换为十进制,赋给 sockVersion。

这一步是为了声明调用不同的WinSock版本。例如MAKEWORD(2,2)就是调用2.2版本,MAKEWORD(1,1) 就是调用1.1版。

不同版本是有区别的,例如1.1版只支持TCP/IP协议,而2.0版可以支持多协议。2.0版有良好的向后兼容性,任何使用1.1版的源代码、二进制文件、应用程序都可以不加修改地在2.0规范下使用。此外 WinSock 2.0 支持异步,1.1不支持异步。

WSADATA 是一个结构体,用于存放 socket 的初始化信息。wsaData 用于存放结构体变量的地址值。

if(WSAStartup(sockVersion,&wsaData)!=0)
	{
		cout<<"WSAStartup() error!"<<endl;
		return 0;
	}

socket 函数的原型为: int socket(int af, int type, int protocol);

socket 函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个 socket 。这个socket 描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

af :即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称 AF_UNIX,Unix 域 socket)、AF_ROUTE 等等协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX 决定了要用一个绝对路径名作为地址。

type:指定 socket 类型。常用的 socket 类型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。

protocol:就是指定协议。常用的协议有,IPPROTO_TCP、PPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC 等,它们分别对应 TCP 传输协议、UDP 传输协议、STCP 传输协议、TIPC 传输协议。

//绑定IP和端口
	sockaddr_in sin;//ipv4的指定方法是使用struct sockaddr_in类型的变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8888);//设置端口。htons将主机的unsigned short int转换为网络字节顺序
	sin.sin_addr.S_un.S_addr = INADDR_ANY;//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址

在上面的套接字类型选择的是 SOCK_STREAM。这种类型需要通信双方均具有地址,其中服务器端的地址需要明确指定,而 ipv4 的指定方法是使用 struct sockaddr_in 类型的变量。

因此我们先实例化一个 sockaddr_in类型的 sin ,用它来进行端口和 IP 地址的设置。

sockaddr_in这个结构体的参数如下:

在程序中,我们用 sin 来分别进行三个参数的设置。具体内容已写在程序的注释中。

    //bind函数把一个地址族中的特定地址赋给scket。
	if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf("bind error !");
	}

bind() 函数把一个地址族中的特定地址赋给 socket。例如对应 AF_INET、AF_INET6 就是把一个 ipv4 或 ipv6 地址和端口号组合赋给 socket。

int bind(SOCKET s, const struct sockaddr * name,int namelen);

SOCKET: 即 socket 描述字,它是通过 socket() 函数创建了,唯一标识一个 socket。bind() 函数就是将给这个描述字绑定一个名字。

sockaddr: 一个 const struct sockaddr *指针,指向要绑定给 sockfd 的协议地址。

namelen: 对应的是地址的长度。 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的 ip 地址组合。这就是为什么通常服务器端在listen 之前会调用 bind(),而客户端就不会调用,而是在 connect() 时由系统随机生成一个。

    //开始监听
	if(listen(slisten,5)==SOCKET_ERROR)
	{
		cout<<"listen error !"<<endl;
		return -1;
	}

作为一个服务器,在调用 socket() 、bind() 之后就会调用 listen() 来监听这个 socket,如果客户端这时调用 connect() 发出连接请求,服务器端就会接收到这个请求。函数原型如下:

int listen(int sockfd, int backlog);

sockfd: 要监听的 socket 描述字。

backlog: 相应 socket 可以排队的最大连接个数。

    //循环接收数据
	SOCKET sclient;
	sockaddr_in remoteAddr;//sockaddr_in常用于socket定义和赋值,sockaddr用于函数参数
	int nAddrlen=sizeof(remoteAddr);
	char revData[255];
	while(true)
	{
		cout<<"等待连接。。。"<<endl;
		sclient=accept(slisten,(sockaddr *)&remoteAddr,&nAddrlen);
		if(sclient==INVALID_SOCKET)
		{
			cout<<"accept error !"<<endl;
			continue;
		}
		cout<<"接收到一个连接:"<<inet_ntoa(remoteAddr.sin_addr)<<endl;
		//接收数据
		int ret=recv(sclient,revData,255,0);
		if(ret>0)
		{
			revData[ret]=0x00;
			cout<<revData<<endl;
		}
		//发送数据
		const char * sendData = "你好,TCP客户端!\n";
		send(sclient, sendData, strlen(sendData), 0);
		closesocket(sclient);
	}

TCP服务器端依次调用 socket()、bind()、listen() 之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket() 、connect() 之后就向 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept() 函数取接收请求,这样连接就建立好了。之后就可以开始网络 I/O 操作了,即类同于普通文件的读写 I/O 操作。

首先看看 accept() 函数的定义:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

不知道大家发现没有,accept 函数的第二个参数定义的是一个sockaddr 的结构体,但是在程序中传入的参数确是由结构体 sockeraddr_in 来定义的,这是为什么呢?

其实, struct sockadd r和 struct sockaddr_in 这两个结构体都是用来处理网络通信的地址。sockaddr 常用于 bind、connect、recvfrom、sendto等 函数的参数,指明地址信息,是一种通用的套接字地址。sockaddr_in 是 internet 环境下套接字的地址形式。所以在网络编程中我们会对 sockaddr_in 结构体进行操作,使用 sockaddr_in 来建立所需的信息,最后使用类型转化就可以了。一般先把 sockaddr_in 变量赋值后,强制类型转换后传入用 sockaddr 做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。

recv() 和 send() 函数分别用于接受和发送数据。如果是在 linux 下,则分别为 read() 和 send() 函数 。

    closesocket(slisten);
closesocket() 函数关闭一个套接口。更确切地说,它释放套接口描述字 s,以后对 s 的访问均以 WSAENOTSOCK 错误返回。若本次为对套接口的最后一次访问,则相应的名字信息及数据队列都将被释放。
WSACleanup();

WSACleanup() 与开头的 WSAStartup() 函数是成对使用的,用于解除与 Socket 库的绑定并且释放 Socket 库所占用的系统资源。

在 Windows 下,Socket 是以 DLL 的形式实现的。在 DLL 内部维持着一个计数器,只有第一次调用 WSAStartup 才真正装载DLL,以后的 调用只是简单的增加计数器,而WSACleanup 函数的功能则刚好相反,每调用一次使计数器减1,当计数器减到0时,DLL 就从内存中被卸载!因此,你调用了多少次 WSAStartup ,就应相应的调用多少次的WSACleanup。

参考:

(初学者的福音)windows下实现socket通信(TCP/IP)代码详解——服务端篇

 

 

recv、send函数windows下超时设置:https://blog.csdn.net/mpp_king/article/details/80248496

参考:
https://blog.csdn.net/mpp_king/article/details/80248496

 

 参考:

https://zhuanlan.zhihu.com/p/551218170

 

 

 

posted @ 2023-11-29 20:22  redrobot  阅读(280)  评论(0编辑  收藏  举报