目的
- 利用WinSock来实现双机通信,理解TCP状态图
- 要求使用WinSock编程,采用其中的TCP面向连接方式,实现文本数据的交换。
环境
- Windows10操作系统、DevC++
- 对于使用MinGW的devc++,是没有ws2_32.lib这个库文件,它对应的库文件是libws2_32.a;可以在项目属性里的链接器添加这个库文件,或者在 工具->编译选项->在连接器命令行加入以下命令 中,删除原有命令,添加 -lws2_32 命令。
所需知识
- 计算机网络通讯
(1)实质:进程之间的通讯
(2)两台计算机之间通讯的基础:主机唯一标识符(IP)、进程唯一标识符(PORT) - Socket简介
(1)Socket是支持TCP/IP 协议的网络通信的基本单元
(2)Socket包含5种信息:连接使用的协议,本机IP,本机PORT,远地主机IP,远地主机PORT。
(3)Socket如何标识唯一进程的:通过IP地址标识网络中的唯一主机,通过传输层协议+PORT唯一标识主机中的进程。
(4)Socket在网络结构中的位置:位于传输层与应用层之间。本质是一个编程接口,封装传输层为应用层提供服务。
(5)Socket架构:C/S架构,典型的客户端、服务端架构。提供数据的传输服务。 - windows socket函数详解
(1)函数库头文件
#include <WinSock2.h>
#pragma comment(lib,"Ws2_32.lib ")
(2)WSAStartup 初始化Ws2_32.dll的函数
int WSAStartup(__in WORD wVersionRequested, __out LPWSADATA lpWSAData);
//WSAStartup 函数用于初始化供进程调用的Winsock相关的dll。
//wVersionRequested标识了用户调用的Winsock的版本号。通常使用MAKEWORD来生成一个版本号,现在一般为2。
//lpWSAData指向WSADATA结构体的指针,lpWSAData返回了系统对Windows Sockets 的描述。
//如果调用成功,WSAStartup 函数返回0。否则,将返回五种错误代码之一。但绝对不能使用WSAGetLastError获取错误代码。
(3)WSACleanup 释放Ws2_32.dll的函数
int WSACleanup(void);
//该函数释放对Winsock链接库的调用。
(4)socket 创建socket的函数
SOCKET WSAAPI socket( __in int af, __in int type,__in int protocol);
//socket函数将创建指定传输服务的socket。
//af:指明地址簇类型,常用的地址簇如下,其余地址簇在Winsock2.h中定义AF_INET(IPv4)、AF_INET6(IPv6)。
//type:指明socket的类型,常见类型:SOCK_STREAM(流套接字,使用TCP协议)、SOCK_DGRAM(数据报套接字,使用UDP协议)、SOCK_RAW(原始套接字)
//protocol:指明数据传输协议,该参数取决于af和type参数的类型。
//如果不出错,socket函数将返回socket的描述符(句柄),否则,将返回INVALID_SOCKET。
(5)bind 服务端将socket与地址关联
int bind( __in SOCKET s, __in const struct sockaddr* name, __in int namelen);
//bind函数将socket关联一个本地地址。
//s:指定一个未绑定的socket。
//name:指向sockaddr地址的指针,该结构含有IP和PORT
//namelen:参数name的字节数。
//无错误返回0,有错误返回SOCKET_ERROR。
(6)listen 服务端网络监听
int listen( __in SOCKET s, __in int backlog );
//s:socket描述符,该socket是一个未连接状态的socket
//backlog:挂起连接的最大长度,如果该值设置为SOMAXCONN,负责socket的底部服务提供商将设置该值为最大合理值。并没有该值的明确规定。
//没有错误发生将返回0,否则返回SOCKET_ERROR
(7) accept服务端connect接收
SOCKET accept(__in SOCKET s,__out struct sockaddr* addr, __in_out int* addrlen );
//accept函数将创建连接。
//s:listen函数用到的socket。
//addr:指向通信层连接实体地址的指针。addr 的格式取决于bind函数内地址簇的类型。
//addrlen:addr的长度
//如果不发生错误,accept将返回一个新的SOCKET描述符,即新建连接的socket句柄。否则,将返回INVALID_SOCKET。
//传进去的addrlen应该是参数addr的长度,返回的addrlen是实际长度。
(8)connect客户端请求服务端连接
int connect( __in SOCKET s,__in const struct sockaddr* name,__in int namelen );
//s:一个没有完成连接的socket;
//name:指向sockaddr地址的指针,该结构含有IP和PORT;
//namelen:参数name的字节数;
//返回0表示正确,否则,将返回SOCKET_ERROR。如果是阻塞式的socket连接,返回值代表了连接正常与失败。
(9)send、recv发送接收数据
int send(__in SOCKET s,__in const char* buf,__in int len,__in int flags );
int recv(__in SOCKET s,__out char* buf, __in int len, __in int flags );
//s:socket
//buf:数据buffer
//len:待发送/接收数据的长度
//flags:send(recv)函数的发送(接收)数据方式。
//send的返回值标识已发送数据的长度,这个值可能比参数len小,这也意味着数据缓冲区没有全部发出去,要进行后续处理。返回SOCKET_ERROR标识send出错。
//recv的返回值标识已接收数据的长度。如果连接已关闭,返回值将是0。返回SOCKET_ERROR标识recv出错。
(10)closesocket关闭socket
closesocket(__in SOCKET s);
//如果无错误发生,函数返回0。否则,返回SOCKET_ERROR。
- socket tcp建立连接,关闭连接以及点对点通讯
(1)建立连接(三次握手)
(2)关闭连接
(3)点对点通信
实验分析
双机通信可以通过一台计算机上运行服务端,一台计算机上运行客户端,通过服务端与客户端通信来实现双机通信。
(1)客户端
客户端需要先初始化Ws2_32.dll的函数,然后建立socket,通过connect()函数与服务端建立连接,如果连接建立成功,客户端需要建立一个用于发送信息的线程来向服务端保证能够随时发送数据,在主线程中需要一直对服务端发送来的信息保持接收。
(2)服务端
服务端要一直等待客户端的连接请求,接受请求后,连接建成,则服务端建立一个线程用于随时向服务端发送数据,同时在主线程中还要一直对客户端发来的信息保持接收。
等待客户请求:
实验结果
首先运行服务端,然后运行客户端,在客户端指定好要连接的服务端的IP地址和端口号则可以与对应服务端连接,然后就可以进行通讯了。
下面中展示的是在我自己电脑上建立了一个服务端一个客户端,它们两个间进行通讯,实际上如果服务端与客户端分别运行在两台电脑上,它们也能成功实现通讯。
代码
服务器端
#include <WinSock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#pragma comment(lib, "ws2_32.lib")
#include<iostream>
using namespace std;
void* recv1(SOCKET sockConn)
{
char recvBuf[10000];
memset(recvBuf, 0, sizeof(recvBuf));
//接收数据
while(true){
int nRecv = ::recv(sockConn, recvBuf, sizeof(recvBuf), 0);
cout<<endl;
if(nRecv>0){
cout<<"server receive:"<<recvBuf<<endl;
}
else break;
}
}
void* send1(void* args)
{
SOCKET sockClient1 = *( (SOCKET*)args );//建立套接字
while(true){
char buff1[10000];
cin>>buff1;
if(buff1 == ""){
break;
}
int e = send(sockClient1, buff1, sizeof(buff1), 0);
if(e == SOCKET_ERROR){
printf("send failed");
break;
}
cout<<"server send:"<<buff1<<endl;
}
}
int main()
{
WSADATA wsaData;
int port = 5099;
char buf[] = "Server: hello, I am a server.....";
if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //加载套接字库
{
printf("Failed to load Winsock");
return 0;
}
//创建用于监听的套接字
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;//IPV4
addrSrv.sin_port = htons(port); //1024以上的端口号
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //INADDR_ANY 代表任意ip,htonl():将主机字节顺序从u_long转换为网络字节
int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
if(retVal == SOCKET_ERROR){
printf("Failed bind:%d\n", WSAGetLastError());
return 0;
}
if(listen(sockSrv,10) ==SOCKET_ERROR){//10代表允许连接的个数
printf("Listen failed:%d", WSAGetLastError());
return 0;
}
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
while(true)
{
//等待客户请求到来
SOCKET sockConn = accept(sockSrv, (SOCKADDR *) &addrClient, &len);
if(sockConn == SOCKET_ERROR){
printf("Accept failed:%d", WSAGetLastError());
break;
}
printf("Accept client IP:[%s]\n", inet_ntoa(addrClient.sin_addr));
//发送数据
int iSend = send(sockConn, buf, sizeof(buf) , 0);
if(iSend == SOCKET_ERROR){
printf("send failed");
break;
}
pthread_t tids[2];
int ret = pthread_create( &tids[0], NULL, send1, (void*)&sockConn );
if( ret != 0 ) //创建线程成功返回0
{
cout << "pthread_create error:error_code=" << ret << endl;
}
recv1(sockConn);
closesocket(sockConn);
}
closesocket(sockSrv);
WSACleanup();
system("pause");
}
客户端:
#include <WinSock2.h>
#include <stdio.h>
#include <pthread.h>
#pragma comment(lib, "ws2_32.lib")
#include<iostream>
using namespace std;
void* recv1(SOCKET sockConn)
{
char recvBuf[10000];
memset(recvBuf, 0, sizeof(recvBuf));//每个字节都用0填充
//接收数据
while(true){
int nRecv = ::recv(sockConn, recvBuf, sizeof(recvBuf), 0);
if(nRecv>0){
cout<<"client receive:"<<recvBuf<<endl;
}
else break;
}
}
void* send1(void* args)
{
SOCKET sockClient1 = *( (SOCKET*)args );
while(true){
char buff1[10000];
cin>>buff1;
if(buff1 == ""){
break;
}
int e = send(sockClient1, buff1, sizeof(buff1), 0);
if(e == SOCKET_ERROR){
printf("send failed");
break;
}
cout <<"client send:"<<buff1<<endl;
}
}
int main()
{
//加载套接字
WSADATA wsaData;
char buff[1024];
memset(buff, 0, sizeof(buff));
if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)//初始化DDL
{
printf("Failed to load Winsock");
return 0;
}
SOCKADDR_IN addrSrv; //服务端地址
addrSrv.sin_family = AF_INET;//IPV4
addrSrv.sin_port = htons(5099);//port
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//serverIP地址,inet_addr将点分十进制地址转换为无符号4字节的整数地址
//创建客户端套接字
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);//创建指定传输服务的socket,流步套接字
if(SOCKET_ERROR == sockClient){
printf("Socket() error:%d", WSAGetLastError());
return 0;
}
//向服务器发出连接请求
if(connect(sockClient, (struct sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET){
printf("Connect failed:%d", WSAGetLastError());
return 0;
}else{
pthread_t tids[2];
int ret = pthread_create( &tids[0], NULL, send1, (void*)&sockClient );
if( ret != 0 ) //创建线程成功返回0
{
cout << "pthread_create error:error_code=" << ret << endl;
}
//接收数据
recv(sockClient, buff, sizeof(buff), 0);
printf("%s\n", buff);
}
recv1(sockClient);
//send1((void*)&sockClient);
//关闭套接字
closesocket(sockClient);
WSACleanup();
}