局域网(LAN)和广域网(WAN)是两种不同范围的计算机网络,它们用于连接多台计算机以实现数据共享和通信。
总的来说,局域网和广域网在范围、连接设备、传输速度以及应用场景上有明显的区别,它们在网络体系中协同工作,为不同规模和需求的组织提供了灵活的网络解决方案。
IP,或称为Internet Protocol(互联网协议),是一种**在计算机网络中用于标识和定位设备(如计算机、路由器、服务器等)的通信协议。**IP地址是在Internet Protocol中使用的标识符,用于唯一标识网络中的每个设备。
# linux
$ ifconfig
# windows
$ ipconfig
# 测试网络是否畅通
# 主机a: 192.168.1.11
# 当前主机: 192.168.1.12
$ ping 192.168.1.11 # 测试是否可用连接局域网
$ ping www.baidu.com # 测试是否可用连接外网
# 特殊的IP地址: 127.0.0.1 ==> 和本地的IP地址是等价的
# 假设当前电脑没有联网, 就没有IP地址, 又要做网络测试, 可用使用 127.0.0.1 进行本地测试
端口是计算机网络中用于**标识进程或服务的抽象概念。在一台计算机上,可以同时运行多个网络应用程序或服务,而端口就是用来区分它们的一种方式**。端口通过数字来标识,范围从0到65535。
网络分层模型是一种将计算机网络功能划分为不同层次的概念性框架,以简化网络设计、管理和维护。其中最经典和广泛使用的分层模型是OSI(开放系统互联)模型和TCP/IP模型。以下是这两个模型的概述:
OSI模型(开放系统互联模型): OSI模型是由国际标准化组织(ISO)定义的一种七层模型,每一层都执行特定的功能,且每一层的功能都建立在下一层提供的服务之上。这七个层次从下到上依次是:
TCP/IP模型: TCP/IP模型是互联网上广泛使用的模型,它包含四个层次,从下到上依次是:
网络接口层(Network Interface Layer): 类似于OSI的物理层和数据链路层,处理硬件设备和网络的连接。
网络层(Internet Layer): 类似于OSI的网络层,负责数据包的路由和转发。(IP)
传输层(Transport Layer): 类似于OSI的传输层,提供端到端的通信控制。(TCP, UDP)
应用层(Application Layer): 同样对应OSI的应用层,提供网络服务和应用程序之间的接口。(HTTP, FTP, DNS)
与套接字相关的函数被包含在头文件sys/socket.h中。
字节序(Byte Order)指的是在计算机中,多字节数据类型的存储顺序(字符串就没有)。具体来说,它指定了在内存中多字节值的字节排列顺序。在多字节数据类型中,比如16位、32位或64位的整数,字节序定义了它们在内存中的存储方式。
有两种主要的字节序:大端序(Big Endian)和小端序(Little Endian)。
0x12345678
:
12 34 56 78
0x12345678
:
78 56 34 12
// 有一个16进制的数, 有32位 (int): 0xab5c01ff
// 字节序, 最小的单位: char 字节, int 有4个字节, 需要将其拆分为4份
// 一个字节 unsigned char, 最大值是 255(十进制) ==> ff(16进制)
内存低地址位 内存的高地址位
--------------------------------------------------------------------------->
小端: 0xff 0x01 0x5c 0xab
大端: 0xab 0x5c 0x01 0xff
在进行跨平台数据交换时,需要考虑字节序的问题,通常使用网络字节序(也称为大端序)来确保数据的正确传输。
从主机字节序到网络字节序的转换函数:htons、htonl
;从网络字节序到主机字节序的转换函数:ntohs、ntohl
。
htons()
:主机字节序到网络字节序的16位整数转换。htonl()
:主机字节序到网络字节序的32位整数转换。#include <arpa/inet.h>
uint16_t hostShort = 0x1234;
uint32_t hostLong = 0x12345678;
uint16_t networkShort = htons(hostShort);
uint32_t networkLong = htonl(hostLong);
ntohs()
:网络字节序到主机字节序的16位整数转换。ntohl()
:网络字节序到主机字节序的32位整数转换。#include <arpa/inet.h>
uint16_t networkShort = 0x3412;
uint32_t networkLong = 0x78563412;
uint16_t hostShort = ntohs(networkShort);
uint32_t hostLong = ntohl(networkLong);
主机字节序的IP地址是字符串, 网络字节序IP地址是整形
虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:
// 主机字节序的IP地址转换为网络字节序
int inet_pton(int af, const char *src, void *dst);
af
:地址族,可以是 AF_INET
表示IPv4,或者 AF_INET6
表示IPv6。src
:以字符串形式表示的IP地址。dst
:用于存储转换后的二进制地址的缓冲区。函数的返回值是一个整数,如果转换成功,则返回 1,如果提供的地址无效,则返回 0,如果发生错误,则返回 -1。
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af
:地址族,可以是 AF_INET
表示IPv4,或者 AF_INET6
表示IPv6。src
:指向包含二进制格式地址的指针。dst
:用于存储转换后的字符串的缓冲区。size
:缓冲区的大小。函数的返回值是一个指向存储在 dst
中的字符串的指针,失败返回 NULL
并设置 errno
。
sockaddr
是一个通用的套接字地址结构,用于在网络编程中表示网络地址信息。
// 在写数据的时候不好用
struct sockaddr {
sa_family_t sa_family; // 地址族协议, ipv4
char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
struct in_addr
{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
这些结构体用于在套接字编程中表示网络地址,通过将 sockaddr_in
结构体的地址转换为 sockaddr
结构体的地址,可以在函数参数中使用通用的 sockaddr
结构。例如,在调用套接字创建函数时,可以将一个 sockaddr_in
结构体的地址强制转换为 sockaddr
结构体的地址,以便在不同的网络函数中使用。
使用套接字通信函数需要包含头文件<arpa/inet.h>
,包含了这个头文件<sys/socket.h>
就不用在包含了。
// 创建一个套接字
int socket(int domain, int type, int protocol);
参数:
返回值:
函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。
// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
返回值:成功返回0,失败返回-1
// 给监听的套接字设置监听
int listen(int sockfd, int backlog);
参数:
返回值:函数调用成功返回0,调用失败返回 -1
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1
这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
参数:
返回值:
如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。
// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
参数:
返回值:
成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
返回值:连接成功返回0,连接失败返回-1
TCP是一个**面向连接的,可靠的,流式传输协议,这个协议是一个传输层协议**。它确保数据在发送和接收之间按顺序、可靠地传输。
socket()
**bind()
listen()
accept()
read()/recv() write()/send()
close()
在tcp的服务器端, 有两类文件描述符
监听的文件描述符
通信的文件描述符
文件描述符对应的内存结构:
服务器端和客户端用文件描述夫建立连接和发送接收数据的流程:
监听的文件描述符:
通信的文件描述符:
在单线程的情况下客户端通信的文件描述符有一个, 没有监听的文件描述符
socket()
connect()
read()/recv() write()/send()
close()
从通信流程中可以看出, 客户端在connect()的时候, 需要知道服务器的IP和端口号, 所以必须要先启动服务器端(bind()函数绑定), 才能进行通信
// server.cpp
// Created by 47468 on 2024/1/19.
//
#include <iostream>
#include "arpa/inet.h"
using namespace std;
#include "string.h"
#include "unistd.h"
int main() {
// 1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
// 2. 将socket()返回值和本地的IP端口绑定到一起
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000);
// INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
// 这个宏可以代表任意一个IP地址
// 这个宏一般用于本地的绑定操作
addr.sin_addr.s_addr = INADDR_ANY;
// inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1){
perror("bind");
exit(0);
}
// 3. 设置监听
ret = listen(lfd, 128);
if(ret == -1){
perror("listen");
exit(0);
}
// 4. 阻塞等待并接受客户端连接
struct sockaddr_in cliaddr;
int clilen = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, (socklen_t*)(&clilen));
if(cfd == -1){
perror("accept");
exit(0);
}
// 打印客户端的地址信息
char ip[24] = {0};
cout << "客户端的ip地址: " << inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip))
<< ", 端口: " << ntohs(cliaddr.sin_port) << endl;
// 5. 和客户端通信
while(1){
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = read(cfd, buf, sizeof(buf));
if(len > 0){
cout << "客户端say: " << buf << endl;
write(cfd, buf, len);
} else if (len == 0){
cout << "客户端断开了连接" << endl;
break;
} else {
perror("read");
break;
}
}
close(lfd);
close(cfd);
return 0;
}
// client.cpp
// Created by 47468 on 2024/1/19.
//
#include <iostream>
#include "arpa/inet.h"
using namespace std;
#include "string.h"
#include "unistd.h"
int main() {
// 1. 创建监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
exit(0);
}
// 2. 链接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // 大端端口
inet_pton(AF_INET, "192.168.110.129", &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1){
perror("connect");
exit(0);
}
// 3. 和服务器通信
int num = 0;
while(1){
// 发送数据
char buf[1024];
sprintf(buf, "你好服务器...%d\n", num++);
write(fd, buf, strlen(buf) + 1);
// 接收数据
memset(buf, 0, sizeof(buf));
int len = read(fd, buf, sizeof(buf));
if(len > 0){
cout << "服务器say: " << buf << endl;
} else if (len == 0){
cout << "服务器端断开了连接" << endl;
break;
} else {
perror("read");
break;
}
sleep(1);
}
close(fd);
return 0;
}
代码测试图示: