目录

一,关于网络编程

1.首先认识一个人——互联网之父,现代网络编程的开创者

2.客户端和服务端

二,socket函数详解

一、什么是协议

二、创建socket

三、TCP 和 UDP

1. TCP vs UDP 核心对比

2. TCP 保证可靠的 7 种方式(面试重点)

4. 使用场景

    本文章内容均来自本人的个人笔记为个人学习总结,禁止他人转载,参考自B站课程:码农论坛《C++环境高级编程》。由于当时方便记笔记,笔记中部分图片来源于原课程视频截图,版权归原作者“码农论坛”及相关权利人所有。对于linux系统,文章中我用的ubuntu,up主用的centos,但原理是相同的,不影响技术学习。

本笔记无任何商业用途(除开csdn官方操作),仅供个人学习交流。感谢原up主的课程分享!

一,关于网络编程

1.首先认识一个人——互联网之父,现代网络编程的开创者

温顿·瑟夫

及其合作者领导了传输控制协议和互联网协议(TCP/IP)的设计和实现。他们制定了网络的基本设计原则,制定了TCP/IP来满足这些需求,建立了TCP/IP原型,并协调了几个早期的TCP/IP实现。温顿·瑟夫与罗伯特·卡恩开发一个网络互联系统——这个系统最终被称为“互联网”。他们于1974年的一篇开创性论文“a Protocol for Packet Network Intercommunication”(分组网络互联协议)中概述了由此产生的互联网架构。温顿·瑟夫在IBM(国际商业机器公司)对Quicktran进行研究,这是一个基于FORTRAN的分时系统。温顿·瑟夫深入参与了阿帕网主机软件(NCP)的讨论和开发,其非正式的、分散的操作模式成为互联网协议开发和开放软件的模型。

2.客户端和服务端

网络通讯是指两台计算机中的程序进行传输数据的过程。注意,是运行在计算机中的程序在通信。不是计算机。

客户程序(端):client指主动发起通讯的程序。

服务程序(端 / 器):server指被动的等待,然后为向它发起通讯的客户端提供服务。

几个例子:

上网:客户端是电脑或手机上的浏览器软件,服务端是 WEB 软件。

QQ:客户端是电脑或手机上的 QQ 软件,服务端是腾讯的后台服务程序。

远程登录 Linux:客户端是 SecureCRT 或其它的软件,服务端是 Linux 的 sshd 服务程序。

有细节要注意:

  1. 客户端必须提前知道服务端的IP地址和通讯端口
  2. 服务端不需要知道客户端的IP地址

演示(假设有两个虚拟机):

其中,138是客户端,139是服务端

分别在这两台虚拟机运行客户端,服务端程序

我用的是ubuntu,可以在同一台ubuntu演示这两个程序,原理是完全一样的,只是服务端的ip地址填服务端虚拟机就行了

学习网络编程,重要的是理解,不是死记函数参数和结构体!

客户端程序:

/*
 * 程序名:demo_client.cpp,此程序用于演示socket的客户端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;

int main(int argc, char* argv[])
{
	if (argc != 3)
	{
		cout << "Using:./demo_client 服务端的IP 服务端的端口\nExample:./demo_client 192.168.101.139 5005\n\n";
		return -1;
	}

	//第1步:创建客户端的socket(套接字)
	// 类比——准备电话
	// 创建客户端的socket,返回套接字描述符(类似文件句柄)
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1)// 创建失败返回-1
	{
		perror("socket");// 打印具体错误原因(如权限、资源不足)
		return -1;
	}
	/*关键参数解释:
	socket 就是让两台电脑上的程序能互相传数据的 “网络管道”
	AF_INET:使用 IPv4 协议(主流网络协议);
	SOCK_STREAM:使用 TCP 协议(面向连接、可靠传输);
	0:默认协议(TCP 协议下可省略);
	sockfd:Socket 描述符,后续所有操作(连接、发送)都基于这个 “句柄”。
	作用:相当于给客户端创建了一个 “网络通讯的通道”,没有这个通道就无法和服务端通信。*/

	//第二步:向服务器发起连接请求。(解析服务端ip并发起连接)   
	// 类比——打电话拨号
	struct hostent* h;   //用于存放服务端ip的结构体
	// 把字符串格式的IP(如"192.168.101.139")转换成网络可识别的结构体
	if ((h = gethostbyname(argv[1])) == NULL) //把字符串格式的ip转换成结构体
	{
		cout << "gethostbyname failed.\n" << endl;
		close(sockfd);// 失败时关闭Socket,释放资源
		return -1;
	}

	//注:gethostbyname:不仅能解析 IP,还能解析域名(如 "www.baidu.com"),返回的h包含 IP 的二进制形式。
	//////填充服务端地址结构体
	struct sockaddr_in servaddr; //用于存放服务端ip和端口结构体
	memset(&servaddr, 0, sizeof(servaddr));// 初始化结构体,避免脏数据
	servaddr.sin_family = AF_INET;// 协议族:IPv4
	// 把解析后的IP复制到结构体中(二进制形式)
	memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);   //指定服务端的ip地址
	// 把端口转换成网络字节序(大端序),赋值给结构体
	servaddr.sin_port = htons(atoi(argv[2]));			  //指定服务端的通信端口
	/* atoi(argv[2]):把字符串端口(如 "5005")转换成整数;
	htons:把主机字节序(小端序,x86 架构)转换成网络字节序(大端序),网络通讯必须统一字节序。*/

	///////向服务端发起连接
	if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0)//向服务端发起连接请求
	{
		perror("connect");// 打印连接失败原因(如服务端未启动、IP/端口错误)
		close(sockfd);
		return -1;
	}
	/* connect:TCP 的 “三次握手” 核心函数,成功返回 0,失败返回 - 1;
	失败场景:服务端未启动、IP 错误、端口被占用、网络不通等。*/

	//第三步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文
	// 打电话->通话
	char buffer[1024];// 数据缓冲区,存储要发送的内容
	for (int i = 0; i < 3; i++)//循环3次,将与服务端进行3次通讯
	{
		int iret;
		memset(buffer, 0, sizeof(buffer));//清空缓冲区
		sprintf(buffer, "这是第%d个超级女生,编号%03d。", i + 1, i + 1);  // 生成请求报文内容。
		// 向服务端发送请求报文:参数=Socket句柄 + 数据缓冲区 + 数据长度 + 标志(0=默认)
		if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
		{
			perror("send");
			break;
		}
		cout << "发送:" << buffer << endl;
		/*sprintf:把格式化内容写入缓冲区(类似 cout,但写入字符数组);
		strlen(buffer):获取有效数据长度(不含字符串结束符 '\0');
		send:发送数据,返回值iret是实际发送的字节数;
		sleep(1):每秒发送 1 条,避免发送过快。*/

		memset(buffer, 0, sizeof(buffer));//把 buffer 数组的所有字节置为 0,清空上一次发送的数据(比如 “这是第 1 个超级女生...”),
		//避免残留数据干扰接收结果。
		//接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待.。
		if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
		{
			cout << "iret=" << iret << endl;
			break;
		}
		// recv() 是从 Socket 连接里 “读取” 对方发过来的数据的函数,像打电话时 “听对方说话”,没数据就等(阻塞),
		// 拿到数据存到缓冲区,拿不到就返回错误 / 断连信号。
		// 返回值:>0 = 收到的字节数,=0 = 对方正常断连,<0 = 接收失败;
		cout << "接收:" << buffer << endl;

		sleep(1);
	}

	//第四步,关闭socket,释放资源
	close(sockfd);
	//作用:断开与服务端的 TCP 连接(四次挥手),释放系统分配的 Socket 资源,必须执行否则会造成资源泄漏。
}

服务端代码:

/*
 * 程序名:demo_server.cpp,此程序用于演示socket通信的服务端
*/
#include <iostream>       // C++标准输入输出(cout)
#include <cstdio>         // C标准输入输出(perror)
#include <cstring>        // 内存操作(memset、strcpy、strlen)
#include <cstdlib>        // 字符串转数字(atoi)
#include <unistd.h>       // 系统调用(close)
#include <netdb.h>        // 网络相关结构体(sockaddr_in)
#include <sys/types.h>    // 系统类型定义(socket相关)
#include <sys/socket.h>   // Socket核心函数(socket/bind/listen/accept/recv/send)
#include <arpa/inet.h>    // 网络字节序转换(htons/htonl)
using namespace std;      // 简化C++标准库调用

//和客户端头文件基本一致,都是 Linux 下 Socket 编程的标配,覆盖 IO、内存、网络核心能力。

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        cout << "Using:./demo_server 通讯端口\nExample:./demo_server 5005\n\n";   // 端口大于1024,不与其它的重复。
        cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";
        cout << "      如果是云服务器,还要开通云平台的访问策略。\n\n";
        return -1;
    }
    /*服务端只需要指定 “监听的端口”(如 5005),不需要指定 IP(后续会绑定到所有网卡);
    关键提醒:端口需用 1024 以上(1024 以下是系统保留端口,普通用户无权使用),且要开放防火墙 / 云平台策略,否则客户端连不上。*/

    //第一步:创建服务端的socket
    //打电话——准备电话机
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
        perror("socket");
        return -1;
    }
    /*AF_INET:IPv4 协议;
    SOCK_STREAM:TCP 协议(面向连接、可靠传输);
    0:默认协议;
    lisenfd:监听套接字描述符,专门用于 “监听客户端连接请求”,不负责实际收发数据。*/

    //第二步:把服务器用于通信的ip和端口绑定到socket上
    // 打电话——分配电话号码
    //服务端需要把 “监听的 IP + 端口” 和套接字绑定,客户端才能找到它。
    struct sockaddr_in servaddr;          // 用于存放服务端IP和端口的数据结构,IPv4专用地址结构体,存储IP+端口
    memset(&servaddr, 0, sizeof(servaddr));// 清空结构体,避免脏数据
    servaddr.sin_family = AF_INET;        // 指定协议。
    // 绑定到所有网卡(0.0.0.0),即服务器的任意IP都能接收连接
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯。
    // 把端口转换成网络字节序,赋值给结构体
    servaddr.sin_port = htons(atoi(argv[1]));     // 指定通信端口,普通用户只能用1024以上的端口。
    /*
    关键参数:
    INADDR_ANY:宏定义,值为 0,代表 “绑定到服务器所有网卡的 IP”(比如服务器有内网 IP、外网 IP,都能接收连接);
    htonl:把主机字节序转换成网络字节序(针对 32 位整数,如 IP);
    htons:把主机字节序转换成网络字节序(针对 16 位整数,如端口);
    atoi(argv[1]):把字符串端口(如 "5005")转成整数。
    
    */

    //绑定服务端的ip和端口
    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0)
    {
        perror("bind");
        close(listenfd);
        return -1;
    }

    // 第三步:把socket设置为可连接(监听)的状态。
    /*
    isten 就是让服务端 socket 开始 “监听连接”,进入可以被客户端连接的状态。
    极简版:
    服务端调用 listen 后,就开始等着客户端来 connect
    同时会维护一个等待连接的队列,避免并发连接丢失
    一句话总结:把 socket 设为监听模式,开启接客模式。
    */
    if (listen(listenfd, 5) != 0)
    {
        perror("listen");
        close(listenfd);
        return -1;
    }

    //第四步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
    int clientfd = accept(listenfd, 0, 0);

    if (clientfd == -1)
    {
        perror("accept");
        close(listenfd);
        return -1;
    }

    cout << "客户端已连接。\n";
    //第五步:与客户端通信,接收客户端发过来的报文后,回复ok
    char buffer[1024];
    while (true)
    {
        int iret;
        memset(buffer, 0, sizeof(buffer));
        //接收客户端的请求报文,如果客户端没有请求报文,recv()函数将阻塞等待
        //如果客户端已经断开连接,recv()函数将返回0
        if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
        {
            cout << "iret=" << iret << endl;
            break;
        }
        cout << "接收:" << buffer << endl;
        strcpy(buffer, "ok");//生成回应报文内容
        //向客户端发送回应报文
        if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
        {
            perror("send");
            break;
        }
        cout << "发送:" << buffer << endl;
    }
    //第六步:关闭socket,释放资源
    close(listenfd);   // 关闭服务端用于监听的socket。
    close(clientfd);   // 关闭客户端连上来的socket。
}

/*
listen函数:
#include <sys/socket.h>
int listen(int sockfd, int backlog);

两个参数:
sockfd
你用 socket() 创建出来的文件描述符
就是服务端自己的 socket
backlog
等待连接的队列长度
简单说:最多能同时排队等待处理的客户端数量
一般写 5、10、128 都行
你写 5 就够了:listen(fd, 5);
*/
/*
accept 与 connect 超直白对比(实习面试必考)
一句话分清
connect:客户端主动去找服务端
accept:服务端等着客户端来找它

1. 谁在用?
connect → 客户端调用
accept → 服务端调用
2. 干什么?
connect
作用:主动连接指定 IP 和端口的服务器
结果:成功后得到一个 socket,可以 send /read
accept
作用:阻塞等待客户端来连接
结果:来一个客户端,就返回一个新的 socket 专门跟它聊天
3. 阻塞表现
connect:
连不上就一直等,直到超时或成功。
accept:
一直死等,没有客户端连接就卡在这里不动。

 面试标准答案(背这个)
connect 是客户端函数,用于主动向服务端发起连接请求。
accept 是服务端函数,用于阻塞等待客户端连接,成功后返回用于通信的客户端 socket。
*/

先简述一下关键参数(后面会细讲):

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd == -1)// 创建失败返回-1

{

perror("socket");// 打印具体错误原因(如权限、资源不足)

return -1;

}

关键参数解释:

AF_INET:使用 IPv4 协议(主流网络协议);

SOCK_STREAM:使用 TCP 协议(面向连接、可靠传输);

0:默认协议(TCP 协议下可省略);

sockfd:Socket 描述符,后续所有操作(连接、发送)都基于这个 “句柄”。

作用:相当于给客户端创建了一个 “网络通讯的通道”,没有这个通道就无法和服务端通信。

Socket函数:

就是让两台电脑上的程序能互相传数据的 “网络管道” —— 比如你电脑的微信(客户端)通过 socket 给腾讯服务器发消息,服务器再通过 socket 把消息传给另一个人的微信。

再补 2 句更透的:

没有 socket:程序只能在自己电脑里跑,没法和外网 / 其他电脑的程序互通;

核心作用:把复杂的网络协议(TCP/IP)封装成简单的 “读写” 操作,你不用管三次握手 / 字节序,只要调用 socket 的发送 / 接收函数,就能跨电脑传数据。

简单记:socket = 程序的 “网络数据线”,一头连自己,一头连对方,能传文字 / 文件 / 图片。

makefile:

想要运行这个网络编程程序,要先看IP地址:

这里有两个网:

一个是127开头的,一个是196开头的

127.x.x.x 是本机专属的 “内部回环地址”(只在本机内通信,不走网卡),196.x.x.x 是局域网内网 IP(走物理网卡,用于跨设备通信)—— 一句话:127 是 “自己跟自己说话”,196 是 “跟局域网里的其他人说话”。

详细对比:

特征

127.x.x.x(以 127.0.0.1 为例)

196.x.x.x(内网 IP)

本质

本地回环地址(系统预留,属于 “虚拟地址”)

局域网内网 IP(网络分配的 “真实门牌号”)

数据走不走网卡

不走!数据只在本机内存里流转,不经过物理网卡

走!数据通过网卡传输,走局域网交换机 / 路由器

通信范围

仅限本机内的程序(比如本机服务端←→本机客户端)

同一局域网内的所有设备(本机、其他电脑、虚拟机)

是否需要网络

不需要!断网也能正常通信

需要!必须连入同一局域网(比如同一 WiFi / 网线)

是否唯一

所有设备的 127.0.0.1 都指向 “自己”,互不干扰

同一局域网内每个设备的 196.x.x.x 都唯一

配置 / 获取方式

系统自带,无需配置,永远可用

由路由器 DHCP 分配,需用ifconfig/ip addr查看

典型用途

本机程序测试(比如你现在的 Socket 本机测试)

跨设备通信(比如物理机连虚拟机、电脑连手机)

两个都可以用。

127.0.0.1:本机内通信的 “懒人地址”,测试优先用,简单无坑;(相当于在同一间房传纸条)

196.x.x.x:跨设备通信的 “局域网地址”,需要网络和正确配置;(相当于在不同房间用墙缝传纸条)用这个还得给特定端口开防火墙(比如5005),比较麻烦

在同一虚拟机,建议用127,不同设备则用196

先运行服务端程序:

再运行客户端程序:

发送的是客户端给服务端的请求报文,接收的是服务端给客户端的回应

运行客户端程序之后,查看服务端:

服务端接收来自客户端的请求报文,发送给客户端的回应报文

流程:

客户send--->服务recv----->服务send----->客户recv

二,socket函数详解

网络编程的每一个细节都很重要,不能一知半解。

一、什么是协议

人与人沟通的方式有很多种:书信、电话、QQ、微信。如果两个人想沟通,必须先选择一种沟通的方式,如果一方使用电话,另一方也应该使用电话,而不是书信。

协议是网络通讯的规则,是约定。

二、创建socket

int socket(int domain, int type, int protocol);

成功返回一个有效的socket,失败返回-1,errno被设置。

全部网络编程的函数,失败时基本上都是返回-1,errno被设置。

只要参数没填错,基本上不会失败。

不过,单个进程中创建的socket数量与受系统参数open files的限制。(ulimit -a )

演示:查看系统参数:

单个进程中,打开文件的最大数量不能超过1024个,socket也是文件描述符,也就是说,打开的文件和创建的socket总数不能超过1024!!!

修改客户端代码,无限生产socket:

编译运行客户端程序:

创建socket最大值是1023,再创建就报错了。

以下是各个参数的解析:

1)domain 通讯的协议家族

PF_INET IPv4互联网协议族。最常用的

PF_INET6 IPv6互联网协议族。

PF_LOCAL 本地通信的协议族。

PF_PACKET 内核底层的协议族。

PF_IPX IPX Novell协议族。

IPv6尚未普及,其它的不常用。

2)type 数据传输的类型

SOCK_STREAM 面向连接的socket:1)数据不会丢失;2)数据的顺序不会错乱;3)双向通道。

怎么理解这个“双向通道”?

无论客户端还是服务端,都可以同时收发数据,发送的数据对面一定能接收,全双工(双向同时)

SOCK_DGRAM 无连接的socket:1)数据可能会丢失;2)数据的顺序可能会错乱;3)传输的效率更高。

3)protocol 最终使用的协议

在IPv4网络协议家族中,数据传输方式为SOCK_STREAM的协议只有IPPROTO_TCP,数据传输方式为SOCK_DGRAM的协议只有IPPROTO_UDP。

本参数也可以填0。

socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    // 创建tcp的socket

socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);    // 创建udp的socket

三、TCP 和 UDP

1. TCP vs UDP 核心对比

维度

TCP

UDP

连接性

面向连接(三次握手建立连接/四次挥手断开连接)

无连接

可靠性

可靠(确认、重传、校验、排序)

不可靠(可能丢、乱、重复)

拥塞控制

通信模式

点对点

一对一 / 一对多 / 多对多

首部大小

20 字节

8 字节

效率

信道

全双工可靠信道

不可靠信道

一句话总结:TCP 要“准”,UDP 要“快”。

TCP把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题

2. TCP 保证可靠的 7 种方式(面试重点)

机制

作用

数据分片

发送端分片,接收端重组,由TCP确定分片的大小并控制分片和重组;

到达确认

每收到一个分片,回复确认包(ACK)

超时重发

超时未收到 ACK,自动重传

滑动窗口

TCP 中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。控制发送速率,窗口为 0 时停止发送

失序处理

接收端重新排序

重复处理

丢弃重复的分片

数据校验

校验和,出错则丢弃

记住:TCP 的可靠,是靠“确认 + 重传 + 窗口 + 排序”换来的。

3. UDP 为什么不可靠

没有上述任何一种机制。如果校验和出错,UDP 直接丢弃报文,不通知发送方,也不重传。

4. 使用场景

场景

选哪个

网页、文件、邮件、支付

TCP

视频/音频直播、实时游戏、广播、DNS,微信视频语音通话

UDP

5. UDP 能实现可靠传输吗

可以,但非常麻烦。

需要在应用层自己实现:

确认机制

超时重传

数据排序

结论:既然 TCP 已经帮我们做好了,99% 的情况直接用 TCP。自己写可靠 UDP,性能和正确性大概率不如 TCP。

一句话总结

TCP 是“打电话”(先连上,再说清楚),UDP 是“对讲机”(喊出去,不确认收到)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐