c++网络编程——第一个网络编程程序、socket函数详解以及TCP,UDP简述
目录
本文章内容均来自本人的个人笔记为个人学习总结,禁止他人转载,参考自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 服务程序。
有细节要注意:
- 客户端必须提前知道服务端的IP地址和通讯端口
- 服务端不需要知道客户端的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 是“对讲机”(喊出去,不确认收到)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)