C++套接字网络编程
OSI、TCP/IP 架构
了解计算机网络后(不了解也行),你应该也必须知道OSI七层网络模型和TCP/IP五层模型。在大学时候,老师们都会叫我们背这些东西,苦不堪言。最后也还是会忘掉,我们应该是学会理解它们之间的关系,而不用记住。
首先粘出图片
这就是他们对应的关系,OSI模型
分的太细,不是我们要考虑的东西,只用知道上三层对应 TCP/IP模型
的应用层,下两层对应 TCP/IP模型
的网络接口层。作为Socket编程,我们处理的就是中间的传输层和网际层的东西。
需要知道TCP、UDP在同一层(传输层)
IP在(网际层)
TCP(Transmission Control Protocol)协议
对于初学者就需要知道它是可靠的、面向连接的协议就行了。连接需要三次握手、断开需要四次挥手。
建立连接 => 三次握手:
三次握手就好比,
面试官邀请你去面试的场景:
第一次:(面试官):您好,您的简历挺不错,明天来面试吗?
第二次:(你):好的,明天我有时间,明天能来。
第三次:(面试官):那好你就明天来面试吧。
这是三次握手的图解:
如上图,TCP是双向连接的分为客户端和服务器。就像我们平时使用 Chrome浏览器
,它就是一个客户端,服务器是各个网站自己家的,我们就不知道了。
第一次:客户端发送SYN报文,请求同步,并发送序列号Seq为 X。
SYN是单词`synchronize`的简写,意为 同步 。就是请求同步的意思。
Seq就是一个序列号。也是单词简写`sequence`。
第二次:服务器收到客户端的SYN报文后,确认要同步。就向客户端发送SYN报文,这个报文和客户端发来的一样,顺带一个ACK报文,ACK报文号是客户端发来的Seq序列号+1,同时附带自己的Seq序列号。
ACK是单词`acknowledge`的缩写,意为 确认 。即收到同步请求,确认同步。
第三次:客户端再次发送ACK确认报文,报文号是服务端第二次握手发来的Seq序列号+1,并发送序列号。
每次都要发送序列号,就是确保连接是正确的,因此TCP是面向连接的,可靠的协议。
问:那为什么不四次握手建立连接呢?
答:其实也可以四次握手建立连接,只是会浪费带宽。而三次握手是必要的。
断开连接 => 四次挥手:
四次挥手就好比,
你拒绝去面试的场景:
第一次:(你):您好,我明天有约了,就不能来面试了。
第二次:(面试官):好的,这边收到您不来面试了?
第三次:(面试官):您这边明天确定不来了吗?
第四次:(你):是的,我不来了。
emmm~~ 现实中面试官肯定不会第三次,还问你是不是不去。但这是计算机断开连接需要的处理。
四次挥手:如上图所示
第一次:客户端要求断开本次连接。向服务器发送 FIN 报文,并携带序列号Seq。
第二次:服务器确认断开连接,发送 ACK 报文,序列号是第一次挥手时序列号+1。
第三次:服务器向客户端发送断开 FIN 报文,并携带序列号。
第四次:客户端确认断开连接,发送 ACK 报文,序列号是第三次挥手时序列号+1。
就是一来一回的发送报文,以确定真的要断开连接。
UDP(User Datagram Protocol) 协议
UDP就不像TCP那样要确认后在发包,它是只管发包,不管你收没收到。这样做的优点就是传输速度快,无情的发包机器。
学习UDP要了解其报文构成
如上图,UDP由首部字段和数据字段组成。
首部字段分为源端口号
、目的端口号
、长度
以及校验码
都是2个字节(16比特)。
源端口号
和目的端口号
很好理解就是发送方和接收方的端口号。
长度
就是UDP数据报的长度。
校验和的作用是检验发送是否出错,出错就丢弃。
IP协议
所谓IP就是一段数字,大家肯定都知道。比如www.baidu.com,这是百度的网址,也可以叫做URL(统一资源定位符),你叫它域名也是一样的。DNS(Domain Name System),即是域名系统。在网上搜索域名解析查询
。输入百度网站的域名,就能解析出一个形如14.215.177.38
的数字。这就是IP。
其中的每一个数字实际上是一个8个的二进制组成的数字,计算机用1比特来存其中的一个数字,总共需要32比特,也就是4个字节数。这也是ip的大小。
Socket
socket编程的概念
socket
就是插座(中文翻译套接字),运行在计算机中的两个程序通过 socket
建立一个通道,数据在通道中运输。
socket
把复杂的TCP/IP协议族隐藏了起来,对程序员来说,只要用好 socket相关的函数,就可以完成网络通信。
socket的分类
socket
提供了流(stream)和数据报(datagram)两种通信机制,即 流socket
和 数据报socket
。
流socket
基于 TCP协议
,是一个有序、可靠、双向字节流的通道,传输数据数据不会丢失、不会重复、顺序也不会错乱。
数据报socket
基于UDP协议,不需要建立和维持连接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但是它的效率比较高。
某些应用层协议,处于历史原因,受当时技术和网络条件限制,选择了基于UDP是实现,其选择的理由很可能现在已经不成立了。
实时的音视频聊天可能采用的是 UDP
,这种业务可以接受数据的丢失且不必重传。
本专题只介绍 流socket
,数据包socket
的应用场景实在太少,以后可能更少。
客户端/服务端模式
在 TCP/IP
网络应用中,两个程序之间通信模式是客户端/服务端模式(client/server)。
CPP 网络编程的示例代码
相关函数
//服务端
socket()
bind()
listen()
accept()
read() 或 recv() 等
write() 或 send() 等
close()
//客户端
socket()
connect()
read() 或 recv() 等
write() 或 send() 等
close()
TCP Stream Socket
代码:
服务器代码:
#include <iostream>
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main(void) {
// 1.初始化套接字库
WORD wVersion;
WSADATA wsaData;
int err;
// 设置版本,可以理解为1.1
wVersion = MAKEWORD(1, 1); // 例:MAKEWORD(a, b) --> b | a << 8 将a左移8位变成高位与b合并起来
// 启动
err = WSAStartup(wVersion, &wsaData);
if (err != 0) {
return err;
}
// 检查:网络低位不等于1 || 网络高位不等于1
if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
// 清理套接字库
WSACleanup();
return -1;
}
// 2.创建tcp套接字 // AF_INET:ipv4 AF_INET6:ipv6
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
// 准备绑定信息
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 设置绑定网卡
addrSrv.sin_family = AF_INET; // 设置绑定网络模式
addrSrv.sin_port = htons(6000); // 设置绑定端口
// hton: host to network x86:小端 网络传输:htons大端
// 3.绑定到本机
int retVal = bind(sockSrv, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR));
if (retVal == SOCKET_ERROR) {
printf("Failed bind:%d\n", WSAGetLastError());
return -1;
}
// 4.监听,同时能接收10个链接
if (listen(sockSrv, 10) == SOCKET_ERROR) {
printf("Listen failed:%d", WSAGetLastError());
return -1;
}
std::cout << "Server start at port: 6000" << std::endl;
SOCKADDR_IN addrCli;
int len = sizeof(SOCKADDR);
char recvBuf[100];
char sendBuf[100];
while (1) {
// 5.接收连接请求,返回针对客户端的套接字
SOCKET sockConn = accept(sockSrv, (SOCKADDR *)&addrCli, &len);
if (sockConn == SOCKET_ERROR) {
//printf("Accept failed:%d", WSAGetLastError());
std::cout << "Accept failed: " << WSAGetLastError() << std::endl;
break;
}
//printf("Accept client IP:[%s]\n", inet_ntoa(addrCli.sin_addr));
std::cout << "Accept client IP: " << inet_ntoa(addrCli.sin_addr) << std::endl;
// 6.发送数据
sprintf_s(sendBuf, "hello client!\n");
int iSend = send(sockConn, sendBuf, strlen(sendBuf) + 1, 0);
if (iSend == SOCKET_ERROR) {
std::cout << "send failed!\n";
break;
}
// 7.接收数据
recv(sockConn, recvBuf, 100, 0);
std::cout << recvBuf << std::endl;
// 关闭套接字
closesocket(sockConn);
}
// 8.关闭套接字
closesocket(sockSrv);
// 9.清理套接字库
WSACleanup();
return 0;
}
客户端代码:
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main(void) {
// 1.初始化套接字库
WORD wVersion;
WSADATA wsaData;
int err;
// 可以理解为1.1
wVersion = MAKEWORD(1, 1); // 例:MAKEWORD(a, b) --> b | a << 8 将a左移8位变成高位与b合并起来
// 启动
err = WSAStartup(wVersion, &wsaData);
if (err != 0) {
return err;
}
// 检查:网络地位不等于1 || 网络高位不等于1
if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
// 清理套接字库
WSACleanup();
return -1;
}
// 创建TCP套接字
SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 服务器地址
addrSrv.sin_port = htons(6000); // 端口号
addrSrv.sin_family = AF_INET; // 地址类型(ipv4)
// 2.连接服务器
int err_log = connect(sockCli, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR));
if (err_log == 0) {
printf("连接服务器成功!\n");
} else {
printf("连接服务器失败!\n");
return -1;
}
char recvBuf[100];
char sendBuf[] = "你好,服务器,我是客户端!";
// 3.发送数据到服务器
send(sockCli, sendBuf, strlen(sendBuf) + 1, 0);
// 4.接收服务器的数据
recv(sockCli, recvBuf, sizeof(recvBuf), 0);
std::cout << recvBuf << std::endl;
// 5.关闭套接字并清除套接字库
closesocket(sockCli);
WSACleanup();
system("pause");
return 0;
}