一.网络编程的基本概念
1.IP地址
IP(Internet Protocol)是网络之间互联的协议,也就是为计算机网络相互连接进行通信而设计的协议。参与网络通信的计算机必须有一个唯一的编号,以便能被其它计算机找到,IP地址就是用来给Internet上的计算机一个唯一的编号,目前IP地址有IPv4和IPv6两个版本。以IPv4为例介绍IP地址的格式,IPv4版本的IP地址是一个32位的二进制数,通常被分割为4个八位的二进制数,每一段的取值为0-255,如“61.135.32.26”,“26.32.54.8”都是合法的IP地址。“127.0.0.0/8”是IPv4定义的本机环回地址,也就是说操作系统内核检测到目标 IP 为127.x.x.x后,数据包会从网络层(IP层)直接折返到接收队列,完全不经过物理网卡、路由表转发以及防火墙的外部链路层规则——这意味着即使你拔掉网线、断开 WiFi,127.0.0.1的通信依然畅通无阻。
2.端口号
一个IP地址对应网络上的一台计算机,但是一台计算机上通常会运行多个网络程序,因此只有IP地址还不够,我们还需要能区分服务器上的每一个网络程序,端口号就是用来区分这些不同的网络程序的。端口号用2个字节的整数表示,所以每台计算机有个端口号,其中系统占用0-1023端口号,要避免使用。
3.TCP
传输控制协议(Transmission Control Protocol,TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,它用于在互联网上,为两台设备之间建立可靠的、按顺序的数据传输通道。
4.套接字
套接字(Socket)是TCP/IP网络编程中的基本操作单元,它通过“IP地址+端口号”的组合唯一标识网络中的通信端点,是不同主机进行双向通信的端点。分为流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)分别对应TCP和UDP协议。
用打电话来来类比就是:
IP地址 = 对方的电话号码(找到哪台设备)。
端口号(Port) = 分机号或具体联系人(找到设备上的哪个程序,比如微信或浏览器)。
TCP = 电话接通后,双方反复确认“你听得清吗?”的可靠通话机制。
而套接字(Socket),就是你手里拿着的那个“电话听筒”——它是你用来发起通话、接听、挂断以及实际传输语音(数据)的那个操作工具。
二.套接字编程的基本步骤
网络上的两个程序通过一条双向通路实现数据交换,这条双向通路的一端称为一个套接字。
TCP套接字编程包括服务器端编程和客户端编程。
1.服务器端编程步骤
(1)加载套接字库,创建服务器端套接字。
(2)绑定套接字到一个IP地址和端口上。
(3)将套接字设置为监听模式等待连接请求,设置为监听套接字。
(4)请求到来时接受连接请求,返回一个新的专门对应此连接的套接字。
(5)用这个新的套接字与客户进行通信,通信结束就关闭这个套接字。
(6)监听套接字继续处于监听状态,等待其它连接请求。
(7)退出程序,先关闭监听套接字,再释放套接字库。
2.客户端编程步骤
(1)加载套接字库,创建客户端套接字。
(2)向服务器发出连接请求。
(3)连接成功后与服务器进行通信。
(4)退出程序,先关闭套接字,再释放套接字库。
三.网络编程的主要函数
1.WSAStartup()
WSAStartup()函数的功能是初始化Winsock DLL库(动态链接库),该库提供了所有Winsock函数,原型如下。
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);参数wVersionRequested指明请求使用的Socket版本,高字节指明副版本,低字节指明主版本。当前主要使用2.2版本的Winsock,对应ws2_32.dll库。
参数lpWSAData用于保存返回的Socket版本信息。
函数调用成功返回0,否则返回错误码。
例如:
WORD wVersionRequested = MAKEWORD(2,0); WSADATA wsaData; int err = WSAStartup(wVisionRequested, &wsaData);MAKEWORD()函数用于将两个byte型数合并为word型数,第一个参数为低8位,第二个参数为高8位。程序请求2.0版本的Winsock。
2.WSACleanup()
WSACleanup()函数的功能是终止Winsock 2 DLL(ws2_32.dll)的使用,原型如下。
int WSACleanup(void);函数没有参数。调用成功返回0,失败返回SOCK_ERROR(-1)。
其核心机制是引用计数,Winsock 在进程中维护着一个全局引用计数器。每成功调用一次WSAStartup() ,计数 +1。每成功调用一次WSACleanup(),计数 -1。只有当计数归零(从 1 变为 0)时,系统才会真正执行以下操作:撤销该进程注册的所有网络资源;强制关闭该进程中所有仍处于打开状态的套接字(类似于自动调用closesocket());卸载协议链,释放底层内核对象。
3.socket()
socket()函数根据参数指定的地址簇(Address Family,决定套接字如何解读地址以及使用哪一类网络协议进行通信),数据类型和协议来分配一个套接字及其所用的资源,原型如下。
SOCKET WSAAPI socket(int af, int type, int protocol);参数af为地址簇,也就是IP地址类型,常用的有AF_INET(IPv4地址)和AF_INET6(IPv6地址)。
参数type指定套接字类型,SOCKET_STREAM为流格式套接字,SOCKET_DGRAM为数据报套接字,SOCKET_RAW为原始套接字。
参数protocol表示传输协议,常用的IPPROTO_TCP指定为TCP传输协议,IPPTOTO_UDP指定为UDP传输协议,0表示由系统自行推导协议类型,系统会根据af + type自动匹配协议类型(AF_INET + SOCK_STREAM + 0 = TCP,AF_INET + SOCK_DGRAM + 0 = UDP)。
4.closesocket()
closesocket()函数的功能是将一个套接字关闭,原型如下。
int closesocket(SOCKET s);参数s是要关闭的套接字。函数成功返回0,失败返回SOCKET_ERROR。
5.bind()
bind()函数的功能是将IP地址和端口绑定到套接字上,原型如下。
int bind(SOCKET s, const struct sockaddr* name, int namelen);参数s指定要绑定的套接字。name是指向sockaddr结构体的指针,可由sockaddr_in强制转换。参数namelen是name的长度。函数成功返回0,失败返回SOCKET_ERROR。
下面来看看sockaddr结构体,其定义如下。
struct sockaddr{ unsigned short sa_family; char sa_data[14]; };其中sa_family是两个字节的协议簇,可以是AF_INET或AF_INET6表示IPv4和IPv6。sa_data用于存放IP地址和端口号,一共14个字节。但是sa_data作为一个整体接受IP地址和端口号,并不方便,于是又定义了另一个结构体sockaddr_in,定义如下。
struct sockaddr_in{ short int sin_family; unsigned short int sin_port; struct in_addr sin_addr; unsigned char sin_zero[8]; };其中sin_family表示地址簇;sin_port表示端口号;sin_addr表示IP地址。其中sin_addr也是结构体占4个字节,这三个成员总共占8个字节,为了和sockaddr具有相同字节数,在最后放8个字节的sin_zero。
结构体in_addr定义如下。
typedef struct in_addr{ union{ struct{ unsigned char s_b1, s_b2, s_b3, s_b4; }S_un_b; struct{ unsigned short s_w1,s_w2; }S_un_w; unsigned long S_addr; }S_un; }IN_ADDR;只有一个共用体成员,分别表示三种存放IP地址的方法:用四个字节;用两个字;用一个长整形。
可以用inet_pton()函数将一个点分十进制文本的IP地址转化为in_addr类型,函数原型如下。
int inet_pton(int af, const char* src, void* dst);其中af是地址簇,参数src是点分十进制文本的IP地址,参数dst用于接收转换结果。
函数成功返回1,返回0表示格式无效即传入错误的IP地址文本,返回-1发生错误。
例如:
SOCKADDR_IN serverAddr; serverAddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); serverAddr.sin_port = htons(8000); if(bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr) == -1){ cout<<"绑定失败!!!"<<endl; WSACleanup(); return 0; }其中htons()函数是将16位整数主机字节序(大端序或小端序)转换为网路字节序(大端序)。
6.listen()
服务器绑定套接字后,再调用listen()函数使套接字进入监听状态,用来监听用户发来的请求。原型如下。
int listen(SOCKET s, int backlog);参数s为监听套接字,backlog为客户端请求队列的最大长度。函数成功返回0,失败返回SOCKET_ERROR。
7.accept()
在监听状态下,调用accept()函数进行接受客户端的连接,即取出队列最前面的客户端请求,并创造一个套接字专门与该客户端通信。其函数原型如下。
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);参数s为监听套接字。
参数addr用于返回新创建的套接字的地址结构。
参数addlen返回新创建的套接字的地址长度。
函数成功返回新的套接字句柄,失败返回INVALIDE_SOCKET。
8.connect()
connect()函数用于客户端请求与服务器的监听套接字连接,原型如下。
int connect(SOCKET s, const struct sockaddr* name, int namelen);参数s是要建立连接的客户端套接字。
参数name是服务器套接字的地址信息。
参数namelen是地址结构体的长度。
函数成功返回0,失败返回SOCKET_ERROR。
9.send()
send()函数用于向TCP连接的另一端发送信息。实际上函数将信息送到socket的缓冲区里,真正的发送由协议完成。原型如下。
int send(SOCKET s, const char *buf, int len, int flags);参数s是发送端的套接字。
参数buf是待发送信息的起始地址。
参数len是请求发送的字节数。
参数flags一般直接填0,表示默认发送行为。
发送成功返回发送的字节数,失败返回SOCKET_ERROR。
10.recv()
recv()函数从TCP连接的另一端接送数据。实际上是从socket的缓冲区将数据复制出来。原型如下。
int recv(SOCKET s, char *buf, int len, int flags);参数s是接收端的套接字。
参数buf指向接收信息的缓冲区。
参数len是缓冲区大小。
参数flags一般是0。
函数成功返回接收的字节数,失败返回SOCKET_ERROR,连接关闭返回0。
四.服务器端与客户端实例
1.服务器端编程
#include<iostream> #include<WinSock2.h> #include<WS2tcpip.h> #pragma comment(lib, "ws2_32") //加载动态链接库 using namespace std; #define SERVER_ADDR "127.0.0.1" //IP地址 #define SERVER_PORT 8000 //端口号 int main() { //初始化Winsock DLL库 WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { cout << "网络初始化失败!!!" << endl; return 0; } //校验版本 if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { printf("Could not find a usable version of Winsock 2.2!!!\n"); WSACleanup(); return 1; } //创建套接字 SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket == INVALID_SOCKET) { cout << "创建socket失败!!!" << endl; WSACleanup(); return 0; } //绑定IP和端口 sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; inet_pton(AF_INET, SERVER_ADDR, &serverAddr.sin_addr); serverAddr.sin_port = htons(SERVER_PORT); if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) { cout << "绑定失败!!!" << endl; WSACleanup(); return 0; } //开始监听(listen()) if (listen(serverSocket, 5) == INVALID_SOCKET) { cout << "监听失败!!!" << endl; WSACleanup(); return 0; } int i = 0; //记录客户端数量 while (true) { //循环接收数据 sockaddr_in clientAddr = { 0 }; int iLen = sizeof(sockaddr_in); //建立专用的通信套接字 SOCKET clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &iLen); if (clientSocket == INVALID_SOCKET) { cout << "监听失败!!!" << endl; WSACleanup(); return 0; } ++i; cout << "第" << i << "个客户端已连接!" << endl; char recvData[64] = { 0 }; int ret = recv(clientSocket, recvData, 64, 0); if (ret > 0) { cout << "接收到客户端数据:" << recvData << endl; char sendData[] = "Welcome Client!!!"; ret = send(clientSocket, sendData, sizeof(sendData), 0); if (ret != sizeof(sendData)) { cout << "发送数据错误!!!" << endl; } } else if (ret == 0) { cout << "客户端已断开连接" << endl; } else { cout << "接受数据出错!!!" << endl; } closesocket(clientSocket); //关闭通信套接字 } closesocket(serverSocket); //关闭服务器套接字 WSACleanup(); return 0; }2.客户端编程
#include<iostream> #include<WinSock2.h> #include<WS2tcpip.h> #pragma comment(lib, "ws2_32") //加载动态链接库 using namespace std; #define SERVER_ADDR "127.0.0.1" //服务器IP地址 #define SERVER_PORT 8000 //服务器端口 int main() { //初始化Winsock DLL库 WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { cout << "网络初始化失败!!!" << endl; return 0; } //创建套接字 SOCKET s = socket(AF_INET, SOCK_STREAM, 0); if (s == INVALID_SOCKET) { cout << "创建socket失败!!!" << endl; WSACleanup(); return 0; } //连接服务器(绑定socket到服务器的IP和端口) sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; inet_pton(AF_INET, SERVER_ADDR, &serverAddr.sin_addr); serverAddr.sin_port = htons(SERVER_PORT); if (connect(s, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) { cout << "连接失败!!!" << endl; WSACleanup(); return 0; } //发送数据 char sendData[] = "Hello Server!"; int ret = send(s, sendData, sizeof(sendData), 0); if (ret != sizeof(sendData)) { cout << "发送数据出错!!!" << endl; WSACleanup(); return 0; } //接收数据 char recvData[64] = { 0 }; ret = recv(s, recvData, 64, 0); if (ret > 0) { cout << "接收到服务器数据:" << recvData << endl; } else if (ret == 0) { cout << "客户端已断开连接" << endl; } else { cout << "接收数据出错!!!" << endl; } closesocket(s); //关闭套接字 WSACleanup(); return 0; }运行时先运行服务器端程序,然后多次运行客户端程序。结果如下。
第1个客户端已连接! 接收到客户端数据:Hello Server! 第2个客户端已连接! 接收到客户端数据:Hello Server! 第3个客户端已连接! 接收到客户端数据:Hello Server! 第4个客户端已连接! 接收到客户端数据:Hello Server! 第5个客户端已连接! 接收到客户端数据:Hello Server! 第6个客户端已连接! 接收到客户端数据:Hello Server!接收到服务器数据:Welcome Client!!!写在最后
非常感谢您能耐心读完这篇关于网络编程的入门分享。如果文中有任何表述不准确、代码不够优雅或逻辑有误之处,恳请各位大佬在评论区不吝指正,我一定虚心学习、及时修正。
如果您觉得这篇文章对您有所帮助,不妨点个赞 👍 并关注我,您的每一次鼓励都是我继续输出的动力。后续我还会分享更多的内容,欢迎常来,一起交流,一起成长! 🚀
——不想干活的十一