本文为华中科技大学 人工智能与自动化学院 自动化1801 魏靖旻编写整理,如有任何错误请及时指正。
如果没有计算机网络基础,建议先看 9、深入理解socket协议 , 10、TCP协议相关介绍。
本文涉及到TCP和UDP协议,编程采用TCP协议,阅读完本文并实践后,可以基本掌握C/C++网络编程。
本文采用的编译器为dev-cpp集成编译环境。使用的函数为Windows下winsock2.h里的函数,Linux上的函数说明和使用方法请参考这篇文章。
在dev-cpp环境下,使用TCP/UDP协议编程应作如下修改(网上很多教程都没有说明如何允许网络协议,不改配置的话dev将会报错)
本文的C语言代码是参考多个CSDN上的代码然后优化而成,C++代码为个人编写而成。所有代码都经过本人测试并确认无误,实现了单电脑简单的QQ交互的过程。
socket,套接字,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
源IP地址和目的IP地址以及源端口号和目的端口号的组合称为套接字。其用于标识客户端请求的服务器和服务。
常用的TCP/IP协议的3种套接字类型如下所示:
数据报套接字(SOCK_DGRAM):
数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。原始套接字(SOCK_RAW):
原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW。
WinSock(Windows Socket)编程依赖于系统提供的动态链接库(DLL),有两个版本:
较早的DLL是 wsock32.dll,大小为 28KB,对应的头文件为 winsock1.h;
最新的DLL是 ws2_32.dll,大小为 69KB,对应的头文件为 winsock2.h。
加载ws2_32.dll
#pragma comment (lib, "ws2_32.lib")
使用DLL之前,还需要调用 WSAStartup() 函数进行初始化,以指明 WinSock 规范的版本
以下是结构指针*LPWSADATA
类型定义:
typedef unsigned short WORD;//minwindef.h
typedef struct WSAData
{
WORD wVersion;
WORD wHighVersion;
#ifdef _WIN64
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char *lpVendorInfo;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
#else
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char *lpVendorInfo;
#endif
} WSADATA, *LPWSADATA;//_wsadata.h
WSAStartup() 函数原型:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
eg:
#define MAKEWORD(a,b) ((WORD) (((BYTE) (((DWORD_PTR) (a)) & 0xff)) | ((WORD) ((BYTE) (((DWORD_PTR) (b)) & 0xff))) << 8))//minwindef.h
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
socket() 函数原型:
WINSOCK_API_LINKAGE SOCKET WSAAPI socket(int af,int type,int protocol);
/*
af ,地址族(Address Family),常用AF_INET(IPv4) 和 AF_INET6(IPv6)。
type ,数据传输方式,常用的有 SOCK_STREAM(面向连接)和 SOCK_DGRAM(无连接)
protocol 表示传输协议,常用的有 IPPROTO_TCP(TCP协议) 和 IPPTOTO_UDP(UDP协议)
*/
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
将套接字与特定的IP地址和端口绑定起来
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;//使用sockaddr_in
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//sockaddr_in强制转换为sockaddr
在最后一步中,我们通过bind函数将sockaddr_in强制转换为sockaddr,接下来我们对这两个结构体进行详细说明:
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};//_ip_types.h
struct in_addr
结构体类型的变量对其中in_addr
结构体,原型如下:
typedef struct in_addr
{
//n_addr_t s_addr; //32位的IP地址
union
{
struct { u_char s_b1, s_b2, s_b3, s_b4; } S_un_b;
struct { u_short s_w1, s_w2; } S_un_w;
u_long S_addr;//typedef unsigned long u_long;
} S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;//inaddr.h
需要inet_addr()
转换
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
bind()
第二个参数的类型为 sockaddr
,而代码中却使用 sockaddr_in
,然后再强制转换为 sockaddr
,这是为什么呢?
sockaddr 结构体的定义如下:
struct sockaddr
{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
下图是 sockaddr 与 sockaddr_in 的对比(括号中的数字表示所占用的字节数):
sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
建立连接,与bind类似,函数原型如下:
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);
套接字进入被动监听状态,其函数原型如下:
int listen(SOCKET sock, int backlog); //Windows
套接字处于监听状态时,接收客户端请求,返回新的套接字,其函数原型如下:
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
发送数据,send函数:
int send(SOCKET sock, const char *buf, int len, int flags);
接收数据,recv函数:
int recv(SOCKET sock, char *buf, int len, int flags);
基本思路:先编译运行server产生中断(监听),后编译运行client。
即需要新建两个C源文件,server.c和client.c。先运行server.c产生监听,然后运行client.c。
在运行源代码前,请先修改如下措施:
工具->编译选项
服务器端 server端:
基本编程思路总结:
首先初始化DLL(WSAStartup),然后创建(socket)套接字(servSock),并将它与特定IP地址绑定(bind),以监听(listen)来自客户端的信息。当客户端发送请求时(connect),服务器端接收到请求(accept),并将老套接字(serSock)IP地址赋给新的套接字(clntSock),之后向客户端发送数据(send)。最后关闭(closesocket)套接字,终止DLL使用(WSACleanup)。
在整个程序运行过程中,涉及四次握手。握手定义参考第九部分的深入理解TCP/UDP协议。
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
int main()
{
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字 PF_INET:IPv4
SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字与特定的IP地址和端口绑定起来
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
sockAddr.sin_port = htons(1234); //端口
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));//searSock与设定的sockAddr绑定
//进入监听状态
listen(servSock, 20);
/*sock 需要进入监听状态的套接字
backlog 请求队列的最大长度*/
//printf("here");//用来调试
//首先运行该程序,则程序运行到此处中断,等待来自客户端的请求
//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);//将server的servSock与客户端的sock相连接(二者地址配置相同),并赋给新的套接字clntSock
/*servsock 为服务器端套接字
clntAddr 为 sockaddr_in 结构体变量
nSize 为参数 addr 的长度,可由 sizeof() 求得*/
//printf("here");//用来调试
//向客户端发送数据
char str[20] = "Hello World!";
send(clntSock, str, strlen(str)+sizeof(char), NULL);//要发送的套接字,要发送的数据地址,要发送的字节数,发送选项
//关闭套接字
closesocket(clntSock);
closesocket(servSock);
WSACleanup();//终止 DLL 的使用
return 0;
}
客户端 client端:
基本编程思路总结:
首先初始化DLL(WSAStartup),然后创建(socket)套接字(sock),配置相应的IP地址(sockAddr),并向服务器端发起请求(connect)。建立连接后,接收来自服务器端的数据(recv)并打印在屏幕上。最后关闭(closesocket)套接字,终止DLL使用(WSACleanup)。
#include
#include
#include
#pragma comment(lib, "ws2_32.lib")
//加载 ws2_32.dll
int main()
{
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//向服务器发起请求
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));//套接字与地址建立连接,与服务器的accept相对应
//接收服务器传回的数据
char szBuffer[MAXBYTE] = {0};
recv(sock, szBuffer, MAXBYTE, NULL);//#define MAXBYTE 0xff(winnt.h)
//输出接收到的数据
printf("Message form server: %s\n", szBuffer);
//关闭套接字
closesocket(sock);
//终止使用 DLL
WSACleanup();
system("pause");
return 0;
}
采用c++实现的过程更加明了。
其中对象有重载,一个是初始的套接字servSock,另一个是将server的servSock与客户端的Sock相连接后(二者地址配置相同),建立的新的套接字clntSock。如对过程有疑问可以参考C语言的实现过程。
server端:
#include
#include
#include
#pragma comment(lib,"ws2_32.lib")//加载 ws2_32.dll
using namespace std;
class Socket
{
private:
SOCKET sock;//套接字
sockaddr_in sockAddr;//特定的IP地址
public:
Socket();//构造servSock
Socket(int n,Socket servSock);//构造clntSock
void usebind2listen();
void usesend(char *str);
void close();
//~Socket();
};
Socket::Socket()
{
this->sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
memset(&(this->sockAddr),0,sizeof(this->sockAddr));
this->sockAddr.sin_family = PF_INET; //使用IPv4地址
this->sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
this->sockAddr.sin_port = htons(1234); //端口
}
Socket::Socket(int n,Socket servSock)
{
this->sock=accept(servSock.sock,(SOCKADDR*)&(this->sockAddr),&n);
//将server的servSock与客户端的sock相连接(二者地址配置相同),并赋给新的套接字clntSock
}
void Socket::usebind2listen()
{
bind(this->sock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));//将servsock与特定地址绑定
listen(this->sock,20);//监听,主程序运行到此处中断
}
void Socket::usesend(char *str)
{
send(this->sock,str,strlen(str)+sizeof(char),0);
}
void Socket::close()
{
closesocket(this->sock);
}
int main(int argc,char**argv)
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
Socket servSock;//构造套接字对象 PF_INET:IPv4
servSock.usebind2listen();//servSock与设定的sockAddr绑定
Socket clntSock(sizeof(sockaddr_in),servSock);//重载构造套接字对象,接收客户端请求
char str[20]="hello world";
clntSock.usesend(str);
servSock.close();
clntSock.close();//关闭套接字
WSACleanup();//终止 DLL 的使用
return 0;
}
client端:
#include
#include
#include
#pragma comment(lib,"ws2_32.lib")//加载 ws2_32.dll
using namespace std;
class CSocket
{
private:
SOCKET sock;//套接字
sockaddr_in sockAddr;//特定的IP地址
public:
CSocket();
void useconnect();
void userecieve(char *str);
void close();
//~CSocket();
};
CSocket::CSocket()
{
this->sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
memset(&(this->sockAddr),0,sizeof(this->sockAddr));
this->sockAddr.sin_family = PF_INET; //使用IPv4地址
this->sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
this->sockAddr.sin_port = htons(1234); //端口
}
void CSocket::useconnect()
{
connect(sock,(SOCKADDR*)&(this->sockAddr),sizeof(SOCKADDR));
//套接字与地址建立连接,与服务器的accept相对应
}
void CSocket::userecieve(char* str)
{
recv(this->sock,str,MAXBYTE,0);
cout<<"message from server:"<<str<<endl;
}
void CSocket::close()
{
closesocket(this->sock);
}
int main(int argc,char**argv)
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
CSocket sock;
sock.useconnect();
char szBuffer[MAXBYTE]={0};
sock.userecieve(szBuffer);
sock.close();
WSACleanup();
system("pause");
return 0;
}
在上面的单电脑信息传输中,适当位置加入循环条件,即可实现你来我往的QQ聊天
退出程序的方式:输入quit
服务器端 server端:
/*
* 服务器端 Server.c
*
*/
#include
#include
#include
#define BUFFSIZE 1024
//#define SOCKET_ERROR (-1) _socket_types.h
int main(int argc, char**argv)
{
int Ret;
WSADATA wsaData;
SOCKET ListeningSocket;
SOCKET NewConnection;
SOCKADDR_IN ServerAddr;
SOCKADDR_IN ClientAddr;
int ClientAddrLen = sizeof(ClientAddr);
unsigned short Port = 5150;
char sendData[BUFFSIZE];
char recvData[BUFFSIZE];
if((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
{
printf("WSASTARTUP_ERROR: %d\n", Ret);
return 0;
}
//创建一个套接字来监听客户机连接
if((ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
{
printf("SOCKET_ERROR: %d\n", INVALID_SOCKET);
return 0;
}
/*
* 填充SOCKADDR_IN结构,这个结构将告知bind我们想要在5150端口监听所有接口上的连接
*/
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(Port); //将端口变量从主机字节顺序转换位网络字节顺序
ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
//ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//使用bind将这个地址信息和套接字绑定起来
if(bind(ListeningSocket, (SOCKADDR *)&ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR)
{
printf("BIND_ERROR: %d\n", SOCKET_ERROR);
return 0;
}
//监听客户机连接。这里使用5个backlog
if(listen(ListeningSocket, 5) == SOCKET_ERROR)
{
printf("LISTEN_ERROR: %d\n", SOCKET_ERROR);
return 0;
}
//连接到达时,接受连接
printf("正在接受连接...");
if((NewConnection = accept(ListeningSocket, (SOCKADDR *)&ClientAddr, &ClientAddrLen)) == INVALID_SOCKET)
{
printf("ACCPET_ERROR: %d\n", INVALID_SOCKET);
closesocket(ListeningSocket);
return 0;
}
printf("检测到一个连接: %s 端口:%d\n", inet_ntoa(ClientAddr.sin_addr), ntohs(ClientAddr.sin_port));
//聊天
while(true)
{
//接收数据
Ret = recv(NewConnection, recvData, BUFFSIZE, 0);
if(Ret > 0)
printf("小民: %s\n", recvData);
else if(Ret < 0)
printf("RECV_ERROR: %d\n", SOCKET_ERROR);
else
{
printf("对方退出程序,聊天结束!");
break;
}
//发送数据
printf("\n小魏:");
scanf("%s", sendData);
if(strcmp(sendData, "quit") == 0) //退出
break;
if(send(NewConnection, sendData, BUFFSIZE, 0) == SOCKET_ERROR)
{
printf("消息发送失败!\n");
break;
}
}
//从容关闭
shutdown(NewConnection, SD_BOTH);
//完成新接受的连接后,用closesocket API关闭这些套接字
closesocket(NewConnection);
closesocket(ListeningSocket);
//应用程序完成对接的处理后,调用WSACleanup
if(WSACleanup() == SOCKET_ERROR)
{
printf("WSACLEANUP_ERROR: %d\n", WSAGetLastError());
return 0;
}
system("pause");
return 0;
}
客户端 client端:
/*
* 客户端 Client.c
*
*/
#include
#include
#include
#define BUFFSIZE 1024
//#define SOCKET_ERROR (-1) _socket_types.h
int main(int argc, char**argv)
{
int Ret;
WSADATA wsaData;
SOCKET s;
SOCKADDR_IN ServerAddr;
unsigned short Port = 5150;
char sendData[BUFFSIZE];
char recvData[BUFFSIZE];
if((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
{
printf("WSASTARTUP_ERROR: %d\n", Ret);
return 0;
}
if((s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
{
printf("SOCKET_ERROR: %d\n", INVALID_SOCKET);
return 0;
}
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(Port);
ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
//ServerAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.101");// 这里S_un.S_addr在不同的IDE中可能不一样,然后IPv4地址使用该程序所运行在的PC上的IPv4地址
if((connect(s, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr))) == SOCKET_ERROR)
{
printf("CONNECT_ERROR: %d\n", SOCKET_ERROR);
closesocket(s);
return 0;
}
//Chat
while(true)
{
printf("\n小民:");
scanf("%s", sendData);
if(strcmp(sendData, "quit") == 0) //quit
break;
if(send(s, sendData, BUFFSIZE, 0) == SOCKET_ERROR)
{
printf("消息发送失败!\n");
break;
}
Ret = recv(s, recvData, BUFFSIZE, 0);
if(Ret > 0)
printf("小魏: %s\n", recvData);
else if(Ret < 0)
printf("RECV_ERROR: %d\n", SOCKET_ERROR);
else
{
printf("对方退出程序,聊天结束!");
break;
}
}
shutdown(s, SD_BOTH);
closesocket(s);
if(WSACleanup() == SOCKET_ERROR)
{
printf("WSACLEANUP_ERROR: %d\n", WSAGetLastError());
return 0;
}
system("pause");
return 0;
}
server端:
#include
#include
#include
#pragma comment(lib,"ws2_32.lib")//加载 ws2_32.dll
#define BUFFSIZE 1024
using namespace std;
class Socket
{
private:
SOCKET sock;//套接字
sockaddr_in sockAddr;//特定的IP地址
unsigned short Port;//端口号
public:
Socket();//构造servSock
Socket(int n,Socket servSock);//构造clntSock
void usebind2listen();
void chat();
void close();
//~Socket();
};
Socket::Socket()
{
this->Port=5150;
this->sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
memset(&(this->sockAddr),0,sizeof(this->sockAddr));
this->sockAddr.sin_family = PF_INET; //使用IPv4地址
this->sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
this->sockAddr.sin_port = htons(Port); //端口
}
Socket::Socket(int n,Socket servSock)
{
this->sock=accept(servSock.sock,(SOCKADDR*)&(this->sockAddr),&n);
//将server的servSock与客户端的sock相连接(二者地址配置相同),并赋给新的套接字clntSock
cout<<"检测到一个连接:"<<inet_ntoa(this->sockAddr.sin_addr)<<"端口:"<<ntohs(this->sockAddr.sin_port)<<endl;
}
void Socket::usebind2listen()
{
bind(this->sock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));//将servsock与特定地址绑定
listen(this->sock,5);//监听,主程序运行到此处中断
cout<<"正在接收连接...";
}
void Socket::chat()
{
int ret;
char sendData[BUFFSIZE];
char recvData[BUFFSIZE];
while(true)
{
//接收数据
ret=recv(this->sock,recvData,BUFFSIZE,0);
if(ret>0)
cout<<"小民:"<<recvData<<endl;
else if(ret<0)
cout<<"recv_error"<<SOCKET_ERROR<<endl;
else
{
cout<<"对方退出程序,聊天结束"<<endl;
break;
}
//发送数据
cout<<endl<<"小魏:";
cin>>sendData;
cin.get();//吃掉回车
if(strcmp(sendData,"quit")==0)
{
break;
}
if(send(this->sock,sendData,BUFFSIZE,0)==SOCKET_ERROR)
{
cout<<"发送失败";
break;
}
}
shutdown(this->sock, SD_BOTH);
}
void Socket::close()
{
closesocket(this->sock);//关闭套接字
}
int main(int argc,char**argv)
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
Socket servSock;//构造套接字对象 PF_INET:IPv4
servSock.usebind2listen();//servSock与设定的sockAddr绑定
Socket clntSock(sizeof(sockaddr_in),servSock);//重载构造套接字对象,接收客户端请求
clntSock.chat();//聊天
servSock.close();
clntSock.close();
WSACleanup();//终止 DLL 的使用
system("pause");
return 0;
}
client端:
#include
#include
#include
#pragma comment(lib,"ws2_32.lib")//加载 ws2_32.dll
#define BUFFSIZE 1024
using namespace std;
class CSocket
{
private:
SOCKET sock;//套接字
sockaddr_in sockAddr;//特定的IP地址
unsigned short Port;//端口号
public:
CSocket();
void useconnect();
void userecieve(char *str);
void chat();
void close();
//~CSocket();
};
CSocket::CSocket()
{
this->Port=5150;
this->sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
memset(&(this->sockAddr),0,sizeof(this->sockAddr));
this->sockAddr.sin_family = PF_INET; //使用IPv4地址
this->sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
this->sockAddr.sin_port = htons(Port); //端口
}
void CSocket::useconnect()
{
connect(sock,(SOCKADDR*)&(this->sockAddr),sizeof(SOCKADDR));
//套接字与地址建立连接,与服务器的accept相对应
}
void CSocket::chat()
{
int ret;
char sendData[BUFFSIZE];
char recvData[BUFFSIZE];
while(true)
{
//发送数据
cout<<endl<<"小民:";
cin>>sendData;
cin.get();//吃掉回车
if(strcmp(sendData,"quit")==0)
{
break;
}
if(send(this->sock,sendData,BUFFSIZE,0)==SOCKET_ERROR)
{
cout<<"发送失败";
break;
}
//接收数据
ret=recv(this->sock,recvData,BUFFSIZE,0);
if(ret>0)
cout<<"小魏:"<<recvData<<endl;
else if(ret<0)
cout<<"recv_error"<<SOCKET_ERROR<<endl;
else
{
cout<<"对方退出程序,聊天结束"<<endl;
break;
}
}
shutdown(this->sock, SD_BOTH);
}
void CSocket::close()
{
closesocket(this->sock);
}
int main(int argc,char**argv)
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
CSocket sock;
sock.useconnect();
sock.chat();
sock.close();
WSACleanup();
system("pause");
return 0;
}
Socket是网络通讯经常采用的一种方式,它不是一个具体的物件也不是想http类的通讯协议。你可以把它看成是一组基于TCP和UDP通信协议的接口,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
好,了解了基本分层机构后,我们现在来形象的理解一下Socket到底是什么。我这里有一个很形象的实例可以帮助我们很好的理解Socket,这也是我当初在学习Socket编程时自己领悟出来的,那就是物流。
我们要传送的数据就是物流中的货物,ip就是发货人和收货人的联系方式,那么Socket是什么?大家想想这送货这这一流程还缺少啥?对呀,物流公司啊,对吧!物流公司就是那个Socket,他负责帮你处理送货的琐碎事情,比如你的货物该怎么打包、该怎么防止易碎物品破碎、该用什么方式运输、等等,如果是境外货物还牵涉到报关等等一系列复杂琐碎的事情,这些物流公司(Socket)帮你处理好了。示意图如下:
然后我们来说一下TCP和UDP的区别。前面讲到Socket就相当于是物流公司,那么TCP协议就相当于是:假设你是寄货方,你要寄给你朋友一份礼物,你首先得去物流公司去寄,这样你再物流公司就相当于建立了一个事物(Socket实例)。然后物流公司有两种服务方式让你选择(TCP和UDP)。
先说TCP,TCP这个协议它比较保守,物流公司说我必须确保你朋友会接收这份礼物,不然它不送。于是物流公司就回去联系你朋友。然后联系内容大致如下:
物流:你好,我这里有个你朋友寄的礼物要给你,你收吗?
你朋友:OK,收,什么时候发,多久到?
物流:现在就发,大概要12小时到。
然后物流才开始发货,并且会给你们一个物流单号,这个单号就是关于这份货物你和你朋友的发货收货凭证。这就是所谓的三次握手。示意图:
物流发送完后还会联系你朋友,大致对话内容:
物流:你好,货物已发送,请注意查收
你朋友:好的。
货物送到,你朋友:OK,货物收到,你们可以结案
物流:好的,我结案了。
这就是所谓的四次挥手(拜拜);
(注释:三次握手四次挥手这都是Socket在你建立的时候根据你选择的协议确定好的)
这样是不是很麻烦,估计也就顺丰有这服务了,其他大部分公司还是没有这么贴心的服务的,他们可能就是UDP协议方式了。
UDP比较简单,当你用物流(Socket)寄货物的时候物流公司并不会管你朋友是否能联系上,是否愿意接收,它只负责将你的货物发出去就完事。这中间是不需要通知你朋友的,你和你朋友也不需要有连接,这就是所谓的无连接。
UDP 常用于一次性传输比较少量数据的网络应用中,UDP 也常用于多媒体应用(如 IP 电话、实时视频会议、流媒体等),显然,可靠数据传输对于这些应用来说并不是最重要的,但 TCP 的拥塞控制会导致数据出现较大的延迟,这是它们不能容忍的,我们最熟悉的视频通话大部分就是用的UDP协议。
TCP面向连接,即发送数据之前需要建立连接,并且TCP是拥有拥塞机制的,当前部分数据还没有被接收完,后部分的数据会被阻塞知道前部分数据传递OK后才会继续传递后半部分,所以通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;在数据完整性、安全性要求较高的场景需要使用TCP协议,比如你传送重要办公文件、图片等,这些东西丢包都是会影响数据品质的。
tcp.flags
tcp.flags.syn == 1
tcp.flags.syn == 1&& tcp.flags.ack == 0
tcp.flags.fin == 1&& tcp.flags.ack == 1
tcp.window_size == 0 && tcp.flags.reset== 1
ip.addr==?.?.?.?
* F : FIN - 结束; 结束会话
* S : SYN - 同步; 表示开始会话请求
* R : RST - 复位;中断一个连接
* P : PUSH - 推送; 数据包立即发送
* A : ACK - 应答
* U : URG - 紧急
一个虚拟连接的建立是通过三次握手来实现的。这三次握手过程如下:
- (B) -> [SYN] -> (A)
假设现在客户端B想与服务器A通信,首先 B 向A发一个SYN (Synchronize) 标记的包,告诉 A 现在要请求建立连接。
注意: 一个 SYN 包就是仅 SYN 标记设为 1 的 TCP 包(参见 TCP 包头Resources)。 认识到这点很重要,只有当 A 收到 B 发来的 SYN 包,才可建立连接,除此之外别无他法。因此,如果你的防火墙丢弃所有的发往外网接口的 SYN 包,那么你将不能与外部任何主机主动建立连接。
- (A) -> [SYN/ACK] -> (B)
接着,A 收到 B 发过来的 SYN 包后会发一个对 SYN 包的确认包(SYN/ACK)
回去,表示对第一个 SYN 包的确认,并继续握手操作。
注意: SYN/ACK 包是仅 SYN 和 ACK 标记为 1 的包。
- (B) -> [ACK] -> (A)
B 收到 SYN/ACK 包,B 发一个确认包(ACK),通知 A 连接已建立。至此,三次握手完成,一个 TCP 连接完成 。
注意: ACK 包就是仅 ACK 标记设为 1 的 TCP 包。 需要注意的是当三次握手完成、连接建立以后,TCP 连接的每个包都会设置 ACK 位 。
关闭已建立的 TCP 连接是通过四次握手实现的,下面是 4 次握手的过程。
(B) -> ACK/FIN -> (A)
(A) -> ACK -> (B)
(A) -> ACK/FIN -> (B)
(B) -> ACK -> (A)
注意: 由于 TCP 连接是双向连接, 因此关闭连接需要在两个方向上做。 ACK/FIN 包(ACK 和 FIN 标记设为 1)通常被认为是 FIN(终结)包.然而, 由于连接还没有关闭, FIN 包总是打上 ACK 标记. 没有 ACK 标记而仅有 FIN标记的包不是合法的包,并且通常被认为是恶意的。
四次握手不是关闭 TCP 连接的唯一方法. 有时,如果主机需要尽快关闭连接(或连接超时,端口或主机不可达),RST (Reset)包将被发送。 注意在,由于 RST包不是 TCP 连接中的必须部分, 可以只发送 RST 包(即不带 ACK 标记). 但在 正常的 TCP 连接中 RST 包可以带 ACK 确认标记 。
1、https://blog.csdn.net/qq_41725312/article/details/90375742#Windows_271
2、https://mp.weixin.qq.com/s?__biz=MzI0ODU0NDI1Mg==&mid=2247492176&idx=1&sn=3be138f895ebaabe778f6a798c9a0bd7&chksm=e99d8cc3deea05d53036aa1651e29cb82f92fc2a6614d9d6c2f1d3b495dd6ca9cd8205243892&mpshare=1&scene=1&srcid=&sharer_sharetime=1574827986780&sharer_shareid=c322363ce8b98030db684603925979c8&key=51b99fa703fecc6bdcbec4658c05c6a71778ad95e9216e57ce0dacd0abc03ab0c1f8828082426dc50d896a10a396e7d2cf56c9d97c5bb831788431c578b4912863f6dce95644fb05a3df31cd7c1f3ef8&ascene=1&uin=MTk0MDc0NTUzMA%3D%3D&devicetype=Windows+10&version=62070158&lang=zh_CN&pass_ticket=Aqe5lFEauGsw14e6yvqQEb1wVB65V8IjWm3%2Fh3qMhXrIGr10eMZqXxUcZBjQzo9Q
3、武汉大学信息管理学院曾同学的TCP的ppt