先介绍一些基本概念:

  • 局域网(LAN):一定区域内的各种计算机、外部设备和数据库连接起来的计算机通信的私有网络,如一个学校、一个公司

  • 广域网(WAN):又称外网、公网,是连接不同地区(国家)局域网或城域网计算机通信的远程公共网络。各国家局域网之间设有防火墙(我国的防火墙叫做长城),来屏蔽一些外网服务器的访问。

  • IP(Internet Protocol):本质上是一个整数,用于表示计算机在网络中的地址,有两个版本:

    • IPV4
      1. 使用一个32位的整型数描述一个IP地址
      2. 可以使用一个点分十进制字符串描述这个IP地址:192.2.194.242 。也可以 用十六进制来表示:0x8002c2f2
      3. 分成了4份,每份一字节,最大值为255,所以最大IP地址为255.255.255.255
      4. 可使用的IP地址为2^32
    • IPV6
      1. 使用一个128位的整形数描述一个IP地址,16字节
      2. 字符串描述:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
      3. 分成了8份,每份两个字节,每一部分用16进制表示
      4. 可使用的IP地址为2^128
      注:linux中查找ip地址命令:ifconfig ; windows中的命令为: ipconfig
  • 端口:端口的作用是定位到主机上的某一个进程,端口也是一个整型数 unsigned short ,一个16位整型,有效端口的取值范围是 0 - 65535。网络通信的进程需要用到端口,一个端口只能给某一进程使用,多个进程不能同时使用同一个端口。
    接下来介绍我们的重点:OSI七层模型与TCP/IP四层模型
    在这里插入图片描述
    在实际应用的时候,我们并不会真的像OSI七层模型一样分成七层实现,而是按照各自的应用将七层另外分层实现,比如上图的TCP四层模型。
    TCP/IP四层模型中每个层都有其对应的协议

    • 应用层:http协议、ftp协议等
    • 传输层:TCP、UDP协议等
    • 网络层:IP协议等
    • 网络接口层:以太网帧协议,如物理层中的IEEE802.2协议
      更具体的可以看下面大佬写的博客
      http://www.ha97.com/3215.html

这里我们主要介绍传输层的TCP通信

  • TCP是一个面向连接的、安全的、流式传输协议
    • 面向连接:是一个双向连接、通过三次握手完成,断开连接需要四次挥手
    • 安全:TCP通信中,会对发送的每个数据包进行检验,如果发现数据丢失会自动重传,直到成功为止。
    • 流式传输:发送端和接收端处理数据的速度可以不一致
      注:以上过程均在内核层调用,只需调用其API即可
  • TCP客户端和服务端的通信流程

    这里借用一下大佬 爱编程的大丙 写的博客中的图,同时非常推荐大家去看他写的文章和视频,强推!~
    链接给上:爱编程的大丙
    ok,我们接着介绍TCP通信流程中的函数

服务端:

①创建套接字,该套接字用于监听是否有客户端请求,起到一个引导员的作用

这里我们先解释下套接字和文件描述符,更加利于后续程序的理解

  • 套接字:标识端口的两个值(IP和端口号)称为一个套接字。从Linux内核的角度来看,一个套接字就是通信的一个端点,从Linux程序的角度来看,套接字就是一个有相应描述符的打开文件。
  • 文件描述符(file description):文件描述符本质上就是一个数组(文件描述符表)的下标,该数组维护着一个进程打开的所有文件。
    • 文件描述符均 ≥ 0; 且前三个文件描述符已经固定 标准输入(0)、标准输出(1)、报错(2)
    • 一个文件描述符拥有两块内存(由内核分配):读缓存和写缓存
    • 每个进程会递增的分配文件描述符,若有fd被回收则会优先在下一次被分配掉。
int lfd = socket(); 
socket函数原型
int socket(int domain, int type, int protocol);
domain:使用何种地址族协议:AF_UNIX本地通信;AF_INET---IPV4;AF_INET6---IPV6
type:SOCK_STREAM(流式通信)此时若protocol为0,则表示用TCP通讯
	 SOCK_DGRAM 此时若protocol为0,则表示用UDP通讯

socket中文译为插座,插座和插头(后面客户端的connect)相结合,构成了一条通道,我们通信就其实就是往通道里面读写信息,相当于往文件里读写信息,此处就可以将这条连接抽象成文件,又因为Linux下一切对象皆为文件,那么用文件描述符(fd)来关联这个文件是不是就显的顺其自然了呢~

②将创建的套接字与PC机IP端口绑定

bind(lfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
此处的第二个参数就是PC机的IP地址和端口号(需要自己给定),第三个参数就是结构体的字节大小

bind函数第二个参数的具体实现如下:

// 在写数据的时候不好用
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
struct sockaddr {
	sa_family_t sa_family;       // 地址族协议, ipv4
	char        sa_data[14];     // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}
struct in_addr
{
    in_addr_t s_addr;
};  

③设置监听

listen(lfd,10);
//函数原型,作用:listen内部维护了一个任务队列
int listen(int sockfd, int backlog);
//注:backlog表示用于监听的文件描述符一次性最多能监听多少个客户端连接请求;最大值128,内核写死

④若监听到客户端请求,创建新的套接字与客户端建立连接

此处是负责监听的文件描述符(lfd)起的作用:

1、新客户端连接请求会发送到lfd的读缓存区

2、函数accept(),会检测读缓存区,若有数据则调用并建立新的连接,否则堵塞(可以通过设置套接字为非阻塞)

int cfd = accept();

//函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd是用于监听的文件描述符,用于指定客户端连接申请
addr:保存的是客户端的IP地址机端口号【由sockfd中的读缓冲区读出】,可以保存,也可以将其设置为NULL
addrlen:存放第二个参数的内存大小即sizeofstruct sockaddr *addr)的地址,当其设置为NULL时,也可以将addrlen设置为NULL
return value:该函数调用成功后返回的是一个>0的整数给到一个用于通信的文件描述符

此处accept函数返回的文件描述符值是一个递增的做法,从3开始(0 - 2是标准输入输出和异常套接字),若后续有文件描述符被回收也是递增分配

通信描述符(cfd):如果有N个客户端与服务端建立连接,就有N对通信描述符(客户端和服务端各一个)

⑤服务器与客户端进行通信函数


读函数原型:
ssize_t read(int sockfd, void *buf, size_t len);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:用于通信的文件描述符;buf:用于存放接收到内容的数组;len:buf的大小即sizeof(buf);flags:描述接收数据的属性,一般填0

写函数原型:
ssize_t write(int sockfd, const void *buf, size_t len);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

注 read和write一对 -- 被包含于头文件 <unistd.h>
   recv和send是一对 -- 被包含于头文件 <sys/socket.h>

返回值是接收到的数据的长度

⑥断开

close(fd);  fd为想要关闭的文件描述符/套接字

客户端

①建立通信描述符

int cfd = socket(); //和服务端一样

②连接函数,与客户端建立连接

connect();
//函数原型
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:即用于网络通信的文件描述符
addr:客户端随机一个端口,为什么随机? 因为服务器端不需要主动去连接客户端
addrlen:sizeofconst struct sockaddr *addr)

③通信

send(); 将数据写入内核,内核再将其写入到cfd的写缓冲,然后内核将其发送出去

recv(); 数据如何进入到内核的程序员不需要关心,只需用通信描述符将数据从读缓存读出即可

④断开

close(cfd);  

(●’◡’●)除此之外,还有一个改变字节序的函数需要介绍

网络通信使用大端字节序,本地存储(PC机)一般用小端存储(Intel和AMD的cpu都是小端)
所以一般要通过网络发送数据时,首先要先将主机字节序转化为网络字节序,然后发送,后面从机接收到数据以后再转换回来

1、端口的转化

#include <arpa/inet.h>
//注: 16位是给端口转换用的,因为端口是16位的
// 32位不是给IP地址转化的
这套api主要用于网络通信过程中 IP 和 端口的转换
//将一个短整型(short)从从机字节序 --> 网络字节序
uint16_t htons(uint_16t hostshort); // h(host) to net(网络) s(short)
//将一个长整型(long)从主机字节序 --> 网络字节序
uint32_t htonl(uint_32t hostlong);

//将一个短整形从网络字节序 --> 主机字节序
uint16_t ntohs(uint16_t  netshort);
//将一个长整型(long)从主机字节序 --> 网络字节序
uint32_t htonl(uint32_t  hostlong);

2、IP地址的转化

//主机字节序的IP地址转换为网络字节序
int inet_pton(int af, const char *src, void *dst);
//af:地址族 - AF_INET: ipv4 格式的 ip 地址 | AF_INET6: ipv6 格式的 ip 地址
//src:传入参数,要转换的点分十进制
//dst:传出参数,转换得到的大端整形IP被写入这块内存中
返回值:成功返回1,失败返回0-1

#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址        
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//size:标记dst指向的内存中最多可以存储多少个字节

//以下两个函数只能处理IPV4的ip地址
//点分十进制IP --> 大端整形
in_addr_t inet_addr(const char *cp);

//大端整形 --> 点分十进制IP
char* inet_ntoa(struct in_addr in);

以上就是TCP服务端和客户端通信需要用到的一些函数。
TCP服务端和新客户端建立连接的过程可以类比为如下图所示的酒店接待模型

客人(新客户端连接请求)来到酒店由lfd(listen fd)这个引导员接到酒店内部,lfd指定(由内核决定)一个专职服务员(服务端的通信套接字)来为客人提供一对一服务(通信)。

总结:

客户端和服务器通过使用套接字接口建立连接,一个套接字是连接的一个端点,连接以文件描述符的形式提供给应用程序,客户端和服务器端就是通过读写这些文件描述符(读/写缓冲区)来实现彼此的通信。

水平有限,如有错误,烦请大家多多指教!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐