大家好,我是白晨,一个不是很能熬夜,但是也想日更的人✈。如果喜欢这篇文章,点个赞,关注一下白晨吧!你的支持就是我最大的动力!
大家好,我是白晨。好久没有更新了,这段时间白晨除了享受愉快的暑假,也在不断学习。本次要给大家分享的就是计算机网络的基础知识,也是帮助大家对网络有一个基本的认识并且可以快速上手写代码。主要内容为:网络应用相关概念、协议模型、网络编程套接字等。
本篇文章对于网络的认识全都是基于Linux操作系统,但是如果没有Linux操作系统的使用经验的同学不影响理解,在更新完网络有关文章后,我准备再出一个讲解Linux的系列文章。本篇文章的编程语言为C/C++。喜欢这篇文章或者想要学习Linux系统的同学,还请不要忘了点一个关注,支持一下白晨吧。
首先,要明确一个概念:操作系统先于网络出现,现在的网络是依附于操作系统的。
- 独立阶段:计算机之间相互独立,数据只能通过硬盘等工具完成传输。
- 网络互联阶段: 多台计算机连接在一起,完成数据共享。可以理解为,需要有一台主机当服务器,其他主机可以访问这台主机,通过这台主机完成数据传输。
- 局域网:通过路由器,交换机等设备完成在一定区域内,所有主机可以实现通信。
- 广域网:是一个和局域网相对的概念,比如:将我国的网络视为广域网,则西安市的网络为局域网;将地球的网络视为广域网,则我国的网络就是一个局域网。
协议就是一种“约定”,比如:在抗战时期,组织要向我方卧底传递情报,规定一声口哨就是行动开始,两声口哨就是行动推迟,三声口哨就是行动取消。上面的例子就是一种协议,而网络协议的提出就是规定网络通信的格式规范以及内容含义,如果地球上的网络交互都使用同一套协议,那么就可以做到全球网络通信。当然,如果你不使用大多数人的协议,那么你就无法与大多数人进行网络通信。
先来看一个例子理解协议分层:
上面的图片表示了两个人打电话进行通信时采用的协议,上层两人都使用汉语协议,底层两人使用电话这样相同的协议(这里抽象的认为电话是底层通话协议的一种),由于两人使用相同的协议,所以可以进行通话。
改变了上层语言的协议,但是上层的改变没有影响下层协议,通信没有被影响。
改变底层通话协议,但是没有影响上层协议,所以通信也没用被影响。
综上可得,可以按照功能划分不同协议(如上例,语言层和通话层按照功能划分了协议),并且其中一层协议改变不会影响其他层协议的使用,这两个特点就是协议分层的重大意义。
我们可以用比较专业的语言概括一下协议分层的特点:分层的最大的好处在于封装。
- OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范。它把网络从逻辑上分为了7层,每一层都有相关、相对应的物理设备。
- OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输。它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯。但是, 它既复杂又不实用。
根据网络通信中,物理设备和功能的不同,自底向上将网络通信的过程分为了七层,每一层都有对应的物理设备或者软件,并且每一层都有不同的协议。虽然OSI协议做出了网络通信的详细规定,但是具体执行过程中,我们发现应用层、表示层、会话层三者很难将其分开,所以,将这三者结合诞生出了TCP/IP协议用在现实实践中。
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。
TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。
- 物理层:负责光/电信号的传递方式。 比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的WIFI无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。
- 数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。有以太网、令牌环网, 无线LAN等标准。交换机(Switch)工作在数据链路层。
- 网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。
- 传输层:负责两台主机之间的数据传输。如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机。
- 应用层:负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层
TCP/IP模型与OSI模型对应关系见下图:
物理层我们考虑的比较少,因此很多时候也可以称为 TCP/IP四层模型。
那么,TCP/IP模型到底对应一台主机的什么层次呢?
我们平时说的网络,其实就是网络协议栈,也就是TCP/IP协议栈,它贯穿了操作系统。
在讲网络传输的基本流程前,我需要先引入几个知识。
报头(数据首部),应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),这种行为称为封装(Encapsulation) 。首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长,上层协议是什么等信息。打个比方,应用层数据就是皇上下的命令,报头就是各级政府向下传递命令是盖的公章。
数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,这个过程叫解包。
根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理,这个过程叫分用。
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame) 。
每层对应报头后的数据被称为有效载荷,而每一层实现封装的过程就是在有效载荷前加上本层报头。
上图数据在网络网络中传输是一个简化的过程,现实中,在以太网传输时,必须通过路由器进行路径选择,直到到达目的主机。
示意过程如下图所示:
- IP协议有两个版本, IPv4和IPv6。我们整个的课程,凡是提到IP协议,没有特殊说明的,默认都是指IPv4。IP地址是在IP协议中,用来标识网络中不同主机的地址。对于IPv4来说, IP地址是一个4字节,32位的整数。
我们通常也使用 “点分十进制” 的字符串表示IP地址, 例如 192.168.0.1,用点分割的每一个数字表示一个字节, 范围是 0 - 255。- IP地址 = 网络号 + 主机号。IP地址一般在网络中用于标识唯一一台主机,网络号用于定位一个网段,主机号用于定位一个网段中的一个主机。
- 发送数据的IP称为源IP,接收数据的IP称为目的IP。
- MAC地址用来识别数据链路层中相连的节点,长度为48位,及6个字节。一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)。MAC地址在网卡出厂时就确定了, 不能修改。MAC地址通常是唯一的(虚拟机中的MAC地址不是真实的MAC地址, 可能会冲突; 也有些网卡支持用户配置MAC地址)
这里要区分一下IP地址和MAC地址:
端口号(Port)是传输层协议的内容,是一个大小为2字节的整数。端口号用来唯一标识一个主机上的一个进程, 作用是告诉操作系统当前的这个数据要交给哪一个进程来处理。
IP地址唯一确定一台主机,端口号唯一确定一个主机上的一个进程,所以**IP地址 + 端口号能够标识网络上的某一台主机的某一个进程**。
一个端口号只能被一个进程占用。注:一个进程可以占用多个端口号,但一个端口号不能绑定多个进程,因为端口号要唯一标识一个进程。
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程,这个概念很重要,后面的套接字编程都是基于这个概念展开的。
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
特点:
传输层协议
面向连接:两个主机使用TCP协议通信时,需要先建立连接,再进行通信。
可靠传输:数据传输是可靠的,如果数据在传输过程中出现意外,TCP协议会有各种措施来帮助数据顺利到达目的主机。
基于字节流:这个先不解释,后面在讲解TCP协议时会展开。
这里就简单的了解一下这个传输层的协议特点,便于后续编程,具体协议内容会出一篇文章专门讲解。
Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。
特点:
传输层协议
无连接:不需要连接就可以直接通信。
不可靠传输:相对于可靠传输,不可靠传输就是直接将数据发到网络上,但是数据是否正常到达以及数据在期间是否有意外该协议该协议不管。
面向数据报
上面这两种协议是两种互为补充的协议,虽然TCP协议可靠,但是TCP协议传输相比于UDP协议较慢,有些直播也采用UDP协议传输以降低延迟。这两种协议在日常生活中都有广泛应用,在不同领域都发挥着不同的作用。
我们知道,数据在内存中的存储方式分大端和小端两种。设想一种场景,一个大端机给小端机发送数据,那么发送到网络上的数据是大端还是小端呢?发送给小端机后,小端机该如何分辨数据大小端呢?这里就需要对于网络上传输的数据的存储方式做一统一规定。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略, 直接发送即可。
库中提供了网络字节序和主机字节序(主机本身的存储方式)相互转换的函数:
h
表示host
,也就是主机,n
表示network
,也就是网络,l
表示32位长整数
,s
表示16位短整数
。例如,
htonl
表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
将函数介绍简单浏览一遍,大概知道每个函数的功能就行,然后继续看后面的编程实例,了解了每个函数的应用场景后再细细看每个函数的参数。
在 Linux 下使用
头文件中 socket()
函数来创建套接字,原型为:
#include
#include
int socket(int domain, int type, int protocol);
参数:
- domain
domain
用于设置网络通信的域,也就是 IP 地址类型,函数socket()
根据这个参数选择通信协议的族。通信协议族在文sys/socket.h
中定义。常用的domain参数有
AF_INET
以及AF_INET6
,分别代表IPv4协议和IPv6协议。
- type
type
为数据传输方式/套接字类型,常用的有SOCK_STREAM
(流格式套接字/面向连接的套接字) 和SOCK_DGRAM
(数据报套接字/无连接的套接字)
- protocol
protocol
用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数设置为0,比如,type
为SOCK_STREAM
时,protocol
默认为TCP协议,而type
为SOCK_DGRAM
时,protocol
默认为UDP协议;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。返回值:
如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
使用实例:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字 int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
在Linux下,使用 bind
函数将已创建的套接字和IP、端口等绑定。一般来说,只有服务器要进行绑定,而客户端不需要进行绑定,操作系统会自动分配。
#include
#include
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
参数:
- sockfd
前面说了
socket
函数调用成功会返回一个文件描述符,这个参数就是传入该套接字的文件描述符。
- my_addr
my_addr
是指向一个结构为sockaddr
参数的指针,sockaddr
中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要将地址结构中的IP地址、端口、类型等结构struct sockaddr
中的成员变量进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等绑定在一起。
- addrlen
addrlen
是my_addr
结构的长度,可以设置成 sizeof(struct sockaddr) 。返回值:
成功返回0,失败返回-1并设置错误码。
使用实例:
配合下面
sockaddr
结构讲解食用最佳。int sock = socket(AF_INET, SOCK_DGRAM, 0);// 创建UDP套接字 if (sock < 0) { std::cerr << "socket failed :" << errno << std::endl; return 1; } uint16_t port = atoi(argv[1]);// 通过命令行参数拿到端口号 // 2. bind IP+PORT struct sockaddr_in local; local.sin_family = AF_INET;// IPv4地址家族 local.sin_port = htons(port);// 主机序列转换为网络序列 // INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。 // 例如在ubuntu的/usr/include/netinet/in.h定义为:#define INADDR_ANY ((in_addr_t) 0x00000000) // linux下的INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。 // 服务器一般不会绑定确定的IP地址,如果一旦绑定确定的IP地址,那么客户端只有访问这个IP地址才能连接到对应端口 // 这样会大大提高服务器的成本,所以一般服务器不绑定确定的IP。 local.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) { std::cerr << "bind failed :" << errno << std::endl; return 2; }
这里要展开说一说 sockaddr
这个结构体,这个结构体的具体结构是:
struct sockaddr
{
unsigned short sa_family; /* 地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET */
char sa_data[14]; /* 14字节协议地址,包括了端口、IP等信息 */
};
此数据结构用做bind、connect、recvfrom、sendto
等函数的参数,指明地址信息。但是一般我们不直接使用 sockaddr
这个结构体,因为它将IP等信息混在了一起,不能很好填写和区分。
我们一般使用 sockaddr_in
这个和 sockaddr
等价的结构体作为参数传递。sockaddr_in
的具体结构如下:
#include
struct sockaddr_in {
short int sin_family; /* 地址家族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址结构体 */
unsigned char sin_zero[8]; /* 不使用,一般用0填充 */
};
struct in_addr
{
in_addr_t s_addr; // IP地址
};
- IPv4和IPv6的地址格式定义在
netinet/in.h
中,IPv4地址用sockaddr_in
结构体表示,包括16位地址类型, 16
位端口号和32位IP地址。- IPv4、 IPv6地址类型分别定义为常数
AF_INET、 AF_INET6
. 这样,只要取得某种sockaddr
结构体的首地址,
不需要知道具体是哪种类型的sockaddr
结构体,就可以根据地址类型字段确定结构体中的内容.- socket API可以都用
struct sockaddr *
类型表示,在传递参数的时候sockaddr_in
需要强制转化成sockaddr
;这样的好
处是程序的通用性,可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数 。
in_addr_t
在头文件
中定义,等价于 unsigned long
,长度为4个字节。也就是说,s_addr
是一个整数,而IP地址是一个字符串,所以需要 inet_addr()
函数进行转换。
下图是 sockaddr
与 sockaddr_in
的结构对比:
可以发现,两者都是16字节,所以可以相互转换使用,由于 sockaddr
结构使用较麻烦,所以一般不使用。
本函数是用于TCP协议的,通常应该在调用socket
和bind
之后,并在调用accept
之前被调用。本函数功能是让 主动连接套接字 变为 被连接套接口 ,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。 在 TCP 服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
#include
#include
int listen(int sockfd, int backlog);
参数:
- sockfd
socket函数返回的网络套接字的文件标识符。
- backlog
这个参数与TCP协议的具体内容有关,前期讲不清楚,但是白晨这里先把这个参数的完整意义写完,等到大家了解了TCP协议再回头看。
Linux内核协议栈为管理一个TCP连接使用两个队列:
半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
全连接队列(用来保存处于ESTABLISHED状态,但是应用层没有调用accept取走的请求)
而全连接队列就和
listen
的第二个参数相关,全连接队列的大小等于 backlog + 1 ,也就是说在不调用accept函数前,能和该服务完成TCP三次握手的连接就最多只有backlog + 1个,而其余的连接申请都不会完成三次挥手。返回值:
成功返回0, 失败返回-1并设置错误码。
使用实例:
// 1. 创建套接字 int sock = socket(AF_INET, SOCK_STREAM, 0); uint16_t port = atoi(argv[1]); // 2. bind struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) { std::cerr << "bind failed : " << errno << std::endl; return 3; } const int backlog = 5; // 3. 设置套接字为listen状态 if (listen(sock, backlog) < 0) { std::cerr << "listen set failed : " << errno << std::endl; return 4; }
当调用完 listen
函数后,listen
函数参数中的套接字将处于监听状态,随时准备和客户端完成连接,而 accept
函数用于将已经完成连接的客户端套接字拿上来准备通信,它将返回一个新的套接字文件描述符,该套接字可以直接与客户端通信。
#include
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd
处于监听状态的套接字描述符。
- addr
这是一个输出型参数,用于获取客户端的信息。
- addrlen
addr结构体大小,一般为sizeof(addr)。
返回值:
成功返回新的套接字描述符,可以直接进行读写与客户端通信。
失败返回-1,并设置错误码。
注:返回的套接字描述符和用于监听的描述符不是一个描述符,返回的套接字描述符用于和客户端通信,不能用于监听,而用于监听的描述符一般不变,一直监听你的端口。
使用实例:
见
网络套接字编程实例
。
客户端用于连接客户端
#include
#include
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
参数:
- sockfd
socket函数创建的套接字描述符。
- serv_addr
指向数据结构
sockaddr
的指针,其中包括目的端口和IP地址。
- addrlen
serv_addr的大小,一般为sizeof(serv_addr)。
返回值:
成功则返回0,失败返回-1并设置错误码。
使用实例:
见
网络套接字编程实例
。
用于发送数据。
#include
#include
// send()函数只能在套接字处于连接状态的时候才能使用。(只有这样才知道接受者是谁)
// send和write的唯一区别就是最后一个参数:flags的存在,当我们设置flags为0时,send和wirte是同等的。
// send一般用于TCP这样面向连接的协议,具体用法和write几乎一样,直接向套接字中写入数据即可。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 与send相比,sendto多了目标地址结构,意味着它可以面向UDP这样的非连接协议。
// dest_addr是要通信的对象的信息
// sendto函数如果在连接状态下使用,后面的两个参数dest_addr和addrlen被忽略的,如果不把他们设置成null和0,会返回错误EISCONN,如果把他们设置成NULL和0,发现实际上不是连接状态,会返回错误EISCONN。正常连接下,它和send同等。
// sendto用法除了要自己传入目标信息外,其余用法和send相似
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
用于数据的接收。
#include
#include
// flags等于0时与read函数用法相同,主要面向连接
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 用法和上文sendto相似,这里不再赘述
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
inet_addr
函数的功能是将“点分十进制”表示的IP地址转换为网络序列的长整形,inet_ntoa
是将 in_addr
类型(in_addr
是 sockaddr_in
成员)的结构体转换为“点分十进制”表示的IP地址。
#include
#include
#include
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
参数:
- cp
要转换的“点分十进制”的IP地址。
- in
要转换的in_addr类型的结构体。
返回值:
inet_addr
的返回值就是一个网络序列的长整形,可以直接用于网络传输。
inet_ntoa
的返回值是一个char*类型的指针,那么这个字符串是怎么创建的?man手册上说,
inet_ntoa
函数,是把这个返回结果放到了静态存储区,不需要我们手动进行释放。但是,当我们多次调用这个函数时,会出现前面的结果被最后一次调用覆盖的情况:
int main() { struct sockaddr_in addr1; struct sockaddr_in addr2; addr1.sin_addr.s_addr = 0; addr2.sin_addr.s_addr = 0xffffffff; char* ptr1 = inet_ntoa(addr1.sin_addr); char* ptr2 = inet_ntoa(addr2.sin_addr); printf("%s\n", ptr1); printf("%s\n", ptr2); return 0; }
输出结果为:
255.255.255.255
255.255.255.255
可见,因为
inet_ntoa
把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。使用示例:
struct sockaddr_in local; local.sin_addr.s_addr = inet_addr("192.168.0.1");// 1. 将点分形式的IP转换为长整型 2. 将主机序列转换为网络序列 char* addr = inet_ntoa(local.sin_addr);// 192.168.0.1
接下来,我会简单的展示两个套接字编程实例,一个基于TCP协议,另一个基于UDP协议。萌新刚接触套接字编程时可能会觉得非常复杂,但是套接字编程其实都是固定的格式,只要把格式记住,套接字编程就简单多了。
UDP协议本次要实现一个建议的命令行解释器,具体效果如下:
具体实现:
// server.cpp
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(char* args)
{
std::cout << args << " server_port" << std::endl;
}
//const int port = 8080;
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return -1;
}
// 1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket failed :" << errno << std::endl;
return 1;
}
uint16_t port = atoi(argv[1]);
// 2. bind IP+PORT
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind failed :" << errno << std::endl;
return 2;
}
// 3. 提供服务
#define NUM 1024
char buf[NUM] = {0};
while (true)
{
struct sockaddr peer;
socklen_t len = sizeof(peer);
// 接受客户端信息
ssize_t sz = recvfrom(sock, buf, NUM - 1, 0, (struct sockaddr *)&peer, &len);
if (sz > 0)
{
buf[sz] = 0;
// 执行buf的命令
std::cout << "client# " << buf << std::endl;
FILE *fp = popen(buf, "r");
// 读取命令的返回信息
std::string echo;
char line[1024] = {0};
while (fgets(line, NUM - 1, fp) != NULL)
{
echo += line;
}
pclose(fp);
// 向客户端发消息
sendto(sock, echo.c_str(), echo.size(), 0, (struct sockaddr *)&peer, len);
}
}
return 0;
}
// client.cpp
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(char *args)
{
std::cout << args << " server_ip server_port" << std::endl;
}
#define NUM 1024
int main(int argc, char *args[])
{
if (argc != 3)
{
Usage(args[0]);
return 1;
}
// 1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket failed :" << errno << std::endl;
return 2;
}
// 2.bind
// 但是客户端不需要绑定端口号,系统会自动分配,同时要明确服务器的端口和ip
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(args[2]));
server.sin_addr.s_addr = inet_addr(args[1]);
// 3. 发送请求
while (true)
{
// std::string s;
// std::cout << "请输入# ";
// std::cin >> s;
std::cout << "MyShell# ";
char line[NUM];
fgets(line, sizeof(line), stdin);
// sendto(sock, s.c_str(), s.size(), 0, (struct sockaddr *)&server, sizeof(server));
sendto(sock, line, strlen(line), 0, (struct sockaddr *)&server, sizeof(server));
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buf[NUM];
ssize_t sz = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&tmp, &len);
if (sz > 0)
{
buf[sz] = 0;
std::cout << buf << std::endl;
}
}
return 0;
}
TCP协议本次要实现的内容也非常简单,客户端连接到服务端后,客户端向服务端发送消息,服务端接收到消息后,将客户端的消息回显出来,并经过处理后发送给客户端,客户端再显示服务端发送的信息。
效果如下:
具体实现:
// server.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
void Usage(char *args)
{
std::cout << "Usage :" << args << " server_port" << std::endl;
}
void ServiceIO(int new_sock)
{
char buf[NUM] = {0};
while (true)
{
bzero(buf, NUM);
ssize_t sz = read(new_sock, buf, NUM - 1);
if (sz > 0)
{
buf[sz] = 0;
std::cout << "client# " << buf << std::endl;
std::string s;
s += ">>server<< ";
s += buf;
write(new_sock, s.c_str(), s.size());
}
else if (sz == 0)
{
std::cout << "client quit" << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// 1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "sock failed : " << errno << std::endl;
return 2;
}
uint16_t port = atoi(argv[1]);
// 2. bind
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind failed : " << errno << std::endl;
return 3;
}
const int backlog = 5;
// 3. 设置套接字为listen状态
if (listen(sock, backlog) < 0)
{
std::cerr << "listen set failed : " << errno << std::endl;
return 4;
}
// 让子进程自动释放
signal(SIGCHLD, SIG_IGN);
// 4. 接受连接,提供服务
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(sock, (struct sockaddr *)&peer, &len); // peer返回连接的客户端的信息
if (new_sock < 0)
{
continue;
}
std::cout << "connecting succeed!" << std::endl;
pid_t id = fork();
if (id == 0)
{
ServiceIO(new_sock);
}
else if (id > 0)
{
}
else
{
std::cerr << "fork error" << std::endl;
continue;
}
}
return 0;
}
// client.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(char *args)
{
std::cout << "Usage : " << args << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
// 1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "sock failed : " << errno << std::endl;
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
// 2. 连接服务器
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
std::cerr << "connect failed!" << std::endl;
return 3;
}
std::cout << "connecting succeed!" << std::endl;
// 3. 发送请求
while (true)
{
std::cout << "Please Enter# ";
#define NUM 1024
char buf[NUM] = {0};
fgets(buf, NUM - 1, stdin);
write(sock, buf, strlen(buf));
ssize_t sz = read(sock, buf, NUM - 1);
if(sz > 0)
{
buf[sz] = 0;
std::cout << buf << std::endl;
}
}
return 0;
}
第一次写网络的文章,内心还是有点紧张,也查阅了不少资料,最后写成了大家看到的这样子。希望大家能从白晨的这篇文章中有所收获,如果这篇文章帮到了你或者有什么意见,还请留个言让白晨知道,以便白晨更好地创作文章。
如果大家有什么想和白晨交流的,欢迎私信白晨。
如果讲解有不对之处还请指正,我会尽快修改,多谢大家的包容。
如果大家喜欢这个系列,还请大家多多支持啦!
如果这篇文章有帮到你,还请给我一个大拇指
和小星星
⭐️支持一下白晨吧!喜欢白晨【网络】系列的话,不如关注
白晨,以便看到最新更新哟!!!
我是不太能熬夜的白晨,我们下篇文章见。