WinSock编程

Winsock介绍

Socket

  • 独立于具体协议的网络编程接口
  • 在ISO模型中,主要位于会话层和传输层之间
  • BSD Socket(伯克利套接字)是通过标准的UNIX文件描述符和其它程序通讯的一个方法,目前已经被广泛移植到各个平台。
  • 不同操作系统中的Socket:Windows Socket (Winsock),Linux Socket (BSD Socket)

Socket类型

  • 流式套接字(SOCK_STREAM):提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
  • 数据报套接字(SOCK_DGRAM):提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
  • 原始套接字(SOCK_RAW):可以对较低层次协议,如IP、ICMP直接访问。

WinSock编程_第1张图片

Windows下的socket简称Winsock,是在Windows环境下使用的一套网络编程接口,基于4.3BSD的BSD Socket API制定
  • 1991年Winsock 1.1,16位,由WINSOCK.DLL支持,主要用在Windows 95中,只支持TCP/IP网络
  • 1997年Winsock 2.2 版,32位,由WSOCK32.DLL支持,主要用在Windows 98及以后的版本中,Winsock2.2增加了对更多协议的支持
已经成为Windows环境下网络编程的事实标准。它在Berkeley接口函数的基础上,还增加了基于消息驱动机制的Windows扩展函数

三类函数
  • 与BSD Socket相兼容的基本函数
  • 与BSD Socket相兼容的网络信息检索函数
  • Windows专用扩展函数
在工程中例用Winsock 

  • 包含头文件 Winsock2.h
  • 导入库ws2_32.lib:1、#pragma comment(lib,”ws2_32.lib”); 2、可以打开工程属性页,配置属性->链接器->输入->附加依赖项中加入ws2_32.lib,以上两种方法任选其一

基本函数

网络连接函数

  • socket 创建套接字
  • bind 绑定本机端口
  • listen 监听端口
  • accept 接受连接
  • connect 建立连接
  • recv, recvfrom 数据接收
  • send, sendto 数据发送
  • closesocket, shutdown 关闭套接    (在linux中用close)

转换函数

IP地址转换函数 

  • inet_addr() 点分十进制数表示的IP地址转换为网络字节序的IP地址
  • inet_ntoa() 网络字节序的IP地址转换为点分十进制数表示的IP地址
字节序函数
  • htonl 4字节主机字节序转换为网络字节序
  • ntohl  4字节网络字节序转换为主机字节序
  • htons 2字节主机字节序转换为网络字节序
  • ntohs 2字节网络字节序转换为主机字节序

WinSock编程_第2张图片

在linux中没有WSAStartup,在linux中recv()可以用read(),send()可以用write().

Windows Socket的启动

使用Winsock API编制的网络应用程序中,在调用任何一个Winsock函数之前都必须检查协议栈安装情况。

函数原型:
int WSAStartup(  WORD wVersionRequested,LPWSADATA lpWSAData   );
函数参数:

  • wVersionRequested是一个WORD型(双字节型)数值,指定使用的版本号,对Winsock2.2而言,此参数的值为0x0202,也可以用宏MAKEWORD(2,2)来获得
  • lpWSAData是一个指向WSADATA结构的指针,它返回关于Winsock实现的详细信息

#include 
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(2,2);
if(WSAStartup(wVersionRequested,&wsaData) != 0)
{
		//Winsock初始化错误
		return;
}
if(wsaData.wVersion != wVersionRequested)
{
		//Winsock版本不匹配
		WSACleanup();
		return;
}
//说明WinsockDLL正确加载,可以执行以下代码
创建套接口socket()
应用程序在使用套接口通信前,必须要拥有一个套接口。
函数原型:SOCKET socket(int af,int type,int protocol);
函数参数:
  • af参数:说明套接字接口要使用的协议地址族,地址族与协议族含义相同。如果想建立一个TCP或UDP,只能用常量AF_INET表示使用互联网协议(IP)地址。
  • type参数:描述套接口的类型,af是AF_INET的时候只能为SOCK_STREAM、SOCK_DGRAM或SOCK_RAW
  • protocol:说明该套接口使用的特定协议,当协议地址族af和协议类型type确定后,协议字段可以使用的值是限定的

WinSock编程_第3张图片

  • 返回值:成功返回一个整型代表创建成功的socket编号;失败返回INVALID_SOCKET,可以使用WSAGetLastError()获得错误编号

指定本地地址-bind()

将套接口与该主机上提供服务的某端口联系在一起。
函数原型:
int bind( SOCKET s,  const struct sockaddr FAR * name,  int namelen);
返回值:成功返回0,失败返回SOCKET_ERROR,使用WSAGetLastError()获得具体的错误号。

函数参数:

  • s标识一个未绑定的套接口描述字,它是socket()函数调用成功时返回的值
  • name是一个与指定协议有关的地址结构指针,存储了套接口的地址信息,Winsock中使用sockaddr_in结构指定IP地址和端口信息     
  • struct sockaddr_in{
    short sin_family;         //sin_family一般为AF_INET,表示使用IP地址族;
    u_short sin_port;            //sin_port是以网络字节序表示的16位端口号;
    struct in_addr sin_addr;           //sin_addr是网络字节序的32位IP地址;
    char sin_zero[8];                //sin_zero字段一般不用,用0填充
             }
  • namelen表示地址参数(name)的长度
  • IP地址参数为INADDR_ANY,则由系统内核来自动指定,port为0,则由系统自动指派一个1024~5000之间惟一的端口号

IP地址转换函数-inet_addr()

把"xxx.xxx.xxx.xxx"的10进制的IP地址转换为32位整数表示方法
函数原型:
unsigned long inet_addr( const char   FAR *cp);
函数参数:cp代表IP地址的点格式的字符串。
返回值:成功返回用32位整数表示的IP地址(按网络字节排列顺序),失败返回INADDR_NONE


服务器端启动监听-listen()

在一个服务器端用socket()调用成功创建了一个套接口,并用bind()函数和一个指定的地址关联后,就需要指示该套接口进入监听连接请求状态,这需要通过listen()函数来实现

函数原型:
int listen(SOCKET s,int backlog);
函数参数:

  • s代表一个已绑定了地址,但还未建立连接的套接口描述字
  • backlog指定了正在等待连接的最大队列长度
返回值:请参考bind()函数


客户端请求连接-connect()

当服务器端建立好套接口并与一个本地地址绑定后,就进入监听状态,等待客户发出连接请求。在客户端套接口建立好之后,就调用connect()函数来与服务器建立连接。
函数原型:
int connect( SOCKET s,const struct sockaddr FAR * name,int namelen);
函数参数:

  • s将要建立连接的套接口描述字
  • name是一个指向远端套接口地址结构(sockaddr_in)的指针,表示s套接口欲与其建立一条连接
  • namelen是服务器端的地址长度,即name的长度
返回值:请参考bind()函数。

在客户端使用该函数请求建立连接时,将激活建立连接的三次握手,用来建立一条到服务器TCP的连接。如果调用该函数前没有调用bind()来绑定本地地址,则由系统隐式绑定一个地址到该套接口

该函数用在UDP的客户端时,connect()函数并不是真正地发出建立请求连接的请求,调用将从本地操作系直接返回。这样可以将服务器的地址信息保存下来,在后续UDP端口发送数据时,由套接口自动在发送函数中填入服务器地址,而不需要由应用程序在调用发送函数时填入.



服务器端接受连接-accept()

在服务器端通过listen()函数调用表示服务器进入监听客户的连接请求状态,而在服务器端调用accept()函数时表示可以接收来自客户端由connect()发出的连接请求,双方进入连接状态。

函数原型:
SOCKET accept(SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen);
函数参数:

  • s标识一个套接字,该套接口处于监听状态
  • addr是一个地址结构的指针,用来存放发出连接请求的那个客户机的IP地址信息
  • addrlen指出客户套接口地址结构的长度

返回值:请参考socket()函数。
备注:该函数用于面向连接的服务器端,在IP协议族中,只用于TCP服务器端

发送数据-send()

在已经建立连接的套接口上发送数据,可以使用send()函数
函数原型:int send( SOCKET s, const char FAR * buf, int len, int flags);
函数参数:

  • s用于标识已建立连接的套接字
  • buf是一个字符缓冲区,内有将要发送的数据
  • len即将发送的缓冲区中的字符数
  • flags用于控制数据传输方式,0表示按正常方式发送数据;宏MSG_DONTROUTE说明系统目标主机就在直接连接的本地网络中,无需路由选择;MSG_OOB指出数据是按带外数据发送的
备注:send()函数适用于已建立连接的数据报或流式套接口发送数据,对于数据报类型套接口必须注意发送数据长度不大于通信子网的IP包最大长度

接收数据-recv()函数

对于已建立连接的套接口来说,要从套接口上接收数据,就要使用recv()函数。
函数原型:int recv(SOCKET s,char FAR * buf,  int len,int flags);
函数参数:

  • s为已建立连接的套接口
  • buf为用于接收数据的缓冲区
  • len为缓冲区的长度
  • flags指定调用的方式。0表示接收的是正常数据,无特殊行为。MSG_PEEK表示会使有用的数据复制到所提供的接收端缓冲区内,但是没有从系统缓冲区中将数据删除。MSG_OOB表示处理带外数据。

关闭读写通道-shutdown()

在一个套接口上的读写操作完成后,应该首先使用shutdown()函数来关闭套接口的读通道、写通道或读写通道,这样做的好处是当双方不再有数据要发送或接收时,可以通知对方,以防止数据丢失,并能“优雅”地关闭连接。
函数原型:
int shutdown(SOCKET s,int how );
返回值:请参考bind()函数。

  • s标识一个套接口的描述字
  • how是一个标志,用于描述禁止哪些操作,取值如下表所示

WinSock编程_第4张图片


关闭套接口-closesocket()

shutdown函数只关闭读写通道,并不关闭套接口,且套接口所占有的资源将被一直保留到closesocket()调用之前。一个套接口不再使用时一定要关闭这个套接口,以释放与该套接口关联的所有资源,包括等候处理的数据。
函数原型:
int closesocket(SOCKET s );
函数参数:s表示即将被关闭的套接口

返回值:请参考bind()函数


无连接的C/S程序工作流程图

WinSock编程_第5张图片

recvfrom()

对于无连接的套接口来说,要从套接口上接收一个数据报并保存发送数据的源地址,就要使用recvfrom()函数。
函数原型:int recvfrom( SOCKET s,char FAR * buf,
int len,int flags,
struct sockaddr FAR * from,
int FAR * fromlen);
函数参数:

  • s标识一个套接口的描述字
  • buf接收数据的缓冲区
  • len接收数据缓冲区的长度
  • flags调用操作方式,同recv()中的flags
  • from可选指针,指向装有源地址的缓冲区
  • fromlen可选指针,指向from缓冲区的长度值
函数说明:该函数的用法与有连接时recv()的用法一致,要注意的是该函数也可以用于有连接时数据的接收

sendto()

对于无连接的套接口来说,要从套接口上发送一个数据报,就要使用sendto()函数
函数原型:int sendto(SOCKET s,const char FAR * buf,
int len,int flags,
const struct sockaddr FAR * to,int tolen);
函数参数:

  • s本机的套接字
  • buf待发送数据的缓冲区
  • len指明buf缓冲区中要发送的数据长度
  • flags调用方式标志位,同send()中的flags
  • to可选指针,指向接收数据的目的套接口地址
  • tolen是to所指的地址的长度
  • 函数说明:该函数的使用方法类似send()函数,当用于无连接套接字接口,调用函数前要设置,指出目标IP地址和目标端口号。如果用于有连接的套接口时,则不能指定目标地址和目标端口,将to设置为空,地址长度设为0。当然在有连接的情况下很少使用该函数

下面是一个简单的服务器端与客户端的实现

//Server
#pragma comment(lib, "ws2_32.lib")
#include 
#include 
using namespace std;


//处理服务
void do_service(SOCKET conn)
{
	char buf[1024] = { 0 };

	while (1)
	{
		//接收
		int ret = recv(conn, buf, sizeof(buf), 0);
		if (ret == SOCKET_ERROR)
		{
			cout << "error with code = " << WSAGetLastError() << endl;
			exit(1);
		}
		if (ret == 0)
		{
			cout << "client close" << endl;
			break;
		}
		if (ret > 0)
		{
			cout << buf << endl;
			send(conn, buf, strlen(buf), 0);  //回射回去
		}

		memset(buf, 0, sizeof buf);  //置0
	
	}
	closesocket(conn);
}

int main(void)
{
	WORD wVersionRequested;  //两个字节,指定使用的版本号
	WSADATA wsaData;     //WSADATA 结构存储 Windows 套接字调用返回的初始化信息对
	int err;

	//makeword(a,b)是将两个byte型合并成一个word型,一个在高8位(b),一个在低8位(a) 
	wVersionRequested = MAKEWORD(2, 2);  //指定版本号

	err = WSAStartup(wVersionRequested, &wsaData);  //启动Windows Socket
	if (err != 0) {
		return 1;
	}

	//检查协议栈的安装信息
	if (LOBYTE(wsaData.wVersion) != 2 ||
		HIBYTE(wsaData.wVersion) != 2) {
		WSACleanup();
		return 1;
	}

	//或者使用下面的方法
	//if (wsaData.wVersion != wVersionRequested)
	//{
	//	//Winsock版本不匹配
	//	WSACleanup();
	//	return 1;
	//}


	SOCKET listenfd;    //创建socket
	listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);  //TCP协议 
	if (listenfd == INVALID_SOCKET)   //创建失败
	{
		cout << "error with code = " << WSAGetLastError() << endl;     //WSAGetLastError()获取错误码
		exit(1);
	}

	sockaddr_in servaddr;   //服务器地址
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;   //地址族
	//servaddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  //指定IP地址  4字节主机字节序转换为网络字节序
	servaddr.sin_port = htons(8888);   //端口号  2字节主机字节序转换为网络字节序

	int ret;  
	int opt = 1;
	ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));  //设置地址重复利用
	if (ret == SOCKET_ERROR)
	{
		cout << "error with code = " << WSAGetLastError() << endl;
		exit(1);
	}


	//绑定
	ret = bind(listenfd, (sockaddr*)&servaddr, sizeof(servaddr));
	if (ret == SOCKET_ERROR)
	{
		cout << "error with code = " << WSAGetLastError() << endl;
		exit(1);
	}

	//监听
	ret = listen(listenfd, SOMAXCONN);
	if (ret == SOCKET_ERROR)
	{
		cout << "error with code = " << WSAGetLastError() << endl;
		exit(1);
	}

	SOCKET conn;
	sockaddr_in peeraddr;
	int peerlen;
	while (1)
	{
		peerlen = sizeof(peeraddr);
		conn = accept(listenfd, (sockaddr*)&peeraddr, &peerlen);
		if (conn == INVALID_SOCKET)
		{
			cout << "error with code = " << WSAGetLastError() << endl;
			exit(1);
		}

		//将一个十进制网络字节序转换为点分十进制IP格式的字符串      2字节网络字节序转换为主机字节序
		cout << inet_ntoa(peeraddr.sin_addr) << " " << ntohs(peeraddr.sin_port) << endl;
		//cout << inetNtop(peeraddr.sin_addr) << " " << ntohs(peeraddr.sin_port) << endl;
		do_service(conn);
	}

	WSACleanup();


	return 0;
}
//Client

#pragma comment(lib, "ws2_32.lib")
#include 
#include 
using namespace std;

int main(void)
{
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;


	wVersionRequested = MAKEWORD(2, 2);   //514

	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0) {
		return 1;
	}

	if (LOBYTE(wsaData.wVersion) != 2 ||
		HIBYTE(wsaData.wVersion) != 2) {
		WSACleanup();
		return 1;
	}

	SOCKET sock;
	sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock == INVALID_SOCKET)
	{
		cout << "1error with code = " << WSAGetLastError() << endl;
		exit(1);
	}

	sockaddr_in servaddr;
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	//servaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	servaddr.sin_port = htons(8888);

	int ret;
	ret = connect(sock, (sockaddr*)&servaddr, sizeof(servaddr));
	if (ret == -1)
	{
		cout << "2error with code = " << WSAGetLastError() << endl;
		exit(1);
	}

	char buf[1024] = { 0 };
	char recvbuf[1024] = { 0 };
	while (1)
	{
		cin >> buf;
		if (strcmp(buf, "quit") == 0)
			break;

		//发送
		ret = send(sock, buf, strlen(buf), 0);
		if (ret == -1)
		{
			cout << "3error with code = " << WSAGetLastError() << endl;
			exit(1);
		}

		//接收
		ret = recv(sock, recvbuf, sizeof(buf), 0);
		if (ret == -1)
		{
			cout << "4error with code = " << WSAGetLastError() << endl;
			exit(1);
		}
		if (ret == 0)
		{
			cout << "server close" << endl;
			break;
		}
		if (ret > 0)
		{
			cout << recvbuf << endl;
		}
		memset(buf, 0, sizeof buf);
		memset(recvbuf, 0, sizeof recvbuf);
	}


	//关闭
	closesocket(sock);

	return 0;
}
对线程见 多线程编程




你可能感兴趣的:(Windows网络编程)