页面加载中 . . .

套接字(Socket)---网络编程


C++套接字网络编程

OSI、TCP/IP 架构

了解计算机网络后(不了解也行),你应该也必须知道OSI七层网络模型和TCP/IP五层模型。在大学时候,老师们都会叫我们背这些东西,苦不堪言。最后也还是会忘掉,我们应该是学会理解它们之间的关系,而不用记住。

首先粘出图片
20221112111602

这就是他们对应的关系,OSI模型分的太细,不是我们要考虑的东西,只用知道上三层对应 TCP/IP模型的应用层,下两层对应 TCP/IP模型的网络接口层。作为Socket编程,我们处理的就是中间的传输层和网际层的东西。

需要知道TCP、UDP在同一层(传输层)

IP在(网际层)

TCP(Transmission Control Protocol)协议

对于初学者就需要知道它是可靠的、面向连接的协议就行了。连接需要三次握手、断开需要四次挥手。

建立连接 => 三次握手:

三次握手就好比,

面试官邀请你去面试的场景:

第一次:(面试官):您好,您的简历挺不错,明天来面试吗?

第二次:(你):好的,明天我有时间,明天能来。

第三次:(面试官):那好你就明天来面试吧。

这是三次握手的图解:

20221112113033

如上图,TCP是双向连接的分为客户端和服务器。就像我们平时使用 Chrome浏览器,它就是一个客户端,服务器是各个网站自己家的,我们就不知道了。

第一次:客户端发送SYN报文,请求同步,并发送序列号Seq为 X。

SYN是单词`synchronize`的简写,意为 同步 。就是请求同步的意思。

Seq就是一个序列号。也是单词简写`sequence`。

第二次:服务器收到客户端的SYN报文后,确认要同步。就向客户端发送SYN报文,这个报文和客户端发来的一样,顺带一个ACK报文,ACK报文号是客户端发来的Seq序列号+1,同时附带自己的Seq序列号。

ACK是单词`acknowledge`的缩写,意为 确认 。即收到同步请求,确认同步。

第三次:客户端再次发送ACK确认报文,报文号是服务端第二次握手发来的Seq序列号+1,并发送序列号。

每次都要发送序列号,就是确保连接是正确的,因此TCP是面向连接的,可靠的协议。
问:那为什么不四次握手建立连接呢?
答:其实也可以四次握手建立连接,只是会浪费带宽。而三次握手是必要的。

断开连接 => 四次挥手:

四次挥手就好比,

你拒绝去面试的场景:

第一次:(你):您好,我明天有约了,就不能来面试了。

第二次:(面试官):好的,这边收到您不来面试了?

第三次:(面试官):您这边明天确定不来了吗?

第四次:(你):是的,我不来了。

emmm~~ 现实中面试官肯定不会第三次,还问你是不是不去。但这是计算机断开连接需要的处理。

20221112112859

四次挥手:如上图所示

第一次:客户端要求断开本次连接。向服务器发送 FIN 报文,并携带序列号Seq。

第二次:服务器确认断开连接,发送 ACK 报文,序列号是第一次挥手时序列号+1。

第三次:服务器向客户端发送断开 FIN 报文,并携带序列号。

第四次:客户端确认断开连接,发送 ACK 报文,序列号是第三次挥手时序列号+1。

就是一来一回的发送报文,以确定真的要断开连接。

UDP(User Datagram Protocol) 协议

UDP就不像TCP那样要确认后在发包,它是只管发包,不管你收没收到。这样做的优点就是传输速度快,无情的发包机器。

学习UDP要了解其报文构成

20221112142029

如上图,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)。

img

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;
}

文章作者: ZhiQ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ZhiQ !
  目录