传统的进程间通信借助内核提供IPC机制进行,但是只能限制于本机通信,若要进行跨机通信,就要使用网络通信。
网络通信的本质是借助内核提供SOCKET伪文件的机制进行通信,实际上是使用了文件描述符,因此需要使用内核提供的socketAPI函数库(在传输层层面进行)。
使用socket会建立一个socket pair,如下图, 一个文件描述符操作两个缓冲区:内核中需要维护两个缓冲区,发送端缓冲区和接收端缓冲区, 这点跟管道是不同的, 管道是两个文件描述符操作一个内核缓冲区。
主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。 在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。
大端:手机、网络 || 小端:电脑
主机字节序列:大端/小端 || 网络字节序列:大端
网络传输用的是大端法,如果机器用的是小端则需要进行大小端转换,Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:
#include
uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序
uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序
uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序
函数名的h表示主机host,n表示网络network,s表示short,l表示long,如果本来不需要转换函数内部就不会转换。
struct sockaddr–存放IP地址,缺点是赋值不方便
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
}
struct sockaddr_in----sockaddr的升级
struct sockaddr_in{
sa_family_t sin_family;// address family: AF_INET
in_port_t sin_port;//port in network byte order
struct in_addr sin_addr;//internet address
}
/* Internet address. */
struct in_addr{
uint32_t s_addr;
}
sa_family 成员是地址族类型(sa_family_t) 的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include
struct sockaddr
{
sa_family_t sa_family;//协议族
char sa_data[14];//数据,没有给出IP地址,就是给了这么一块儿空间,起了一个占位
的作用.
};
sa_family 成员是地址族类型(sa_family_t) 的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于 IPV4 和 IPV6:
//sin_family: 地址族 AF_INET
//sin_port: 端口号,需要用网络字节序表示
//sin_addr: IPV4 地址结构: s_addr 以网络字节序表示 IPV4 地址
struct in_addr
{
u_int32_t s_addr;//无符号的32位的整型,存放IP地址;
};
//tcp协议族
struct sockaddr_in
{
sa_family_t sin_family;//地址族,就是sin_family: 地址族 AF_INET
u_int16_t sin_port;//端口,16位的端口
struct in_addr sin_addr;//一个结构体,只有一个成员,是无符号的32位的整型,
存放IP地址;(IPV4的地址就是32位)
//其实后面还有占位的,只是我们不用它,所以就没有写;
};
//tcp协议族就主要有三个:地址族,端口号,IP地址
//IP协议族
struct in6_addr
{
unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示
};
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址族: AF_INET6
u_inet16_t sin6_port; // 端口号:用网络字节序表示
u_int32_t sin6_flowinfo; // 流信息,应设置为 0
struct in6_addr sin6_addr; // IPV6 地址结构体
u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
};
通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换
#include
in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网
络字节序
char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符
串表示
函数名中p表示点分十进制的字符串形式,to表示到,n表示network
socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM) 。
int socket(int domain, int type,int protocal);
函数描述 创建socket
参数说明
(1)domain–协议版本
AF_INET OPV4 AF_INET6 IPV6 AF_UNIX AF_LOCAL(本地套接字使用)
(2)type–协议类型
SOCK_STREAM–流式,默认使用TCP协议
SOCK_DGRAM–报式,默认使用UDP协议
(3)protocol
一般填0,表示使用对应类型的默认协议
返回值
成功返回一个大于0的监听文件描述符,该文件描述符用于监听客户端连接,失败返回 -1,并设置errno
当调用socket函数以后, 返回一个监听文件描述符, 内核会提供与该文件描述符相对应的读和写缓冲区, 同时还有两个队列, 分别是请求连接队列和已连接队列(前提是在监听队列的时候才会存在);
因此无法直接操作内核,而是对文件描述符进行操作,如read/write/close等。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
访问服务器需要绑定一个端口,不绑定端口也可以,但每次启动端口所获得的端口号式随机的,对方不知道发送数据的一方,无法对发送方进行访问。
bind()方法是用来指定套接字使用的 IP 地址和端口。 IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16位的整形值,一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次, 1024-4096 为保留端口, 用户一般也不使用。 4096 以上为临时端口,用户可以使用。在Linux 上, 1024 以内的端口号,只有 root 用户可以使用。
函数描述 将socket文件描述符和IP、PORT端口号绑定
参数说明
(1)sockfd
调用socket函数返回的文件描述符
(2)addr
本地服务器的IP地址和PORT,存放IP端口
struct sockaddr_in serv;
serv.sin_family = AF_INET;
ser.sin_port = htons(8888);
//serv.sin_addr.s_addr = htonl(INADDR_ANY);
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
(3)addrlen
addr变量占用的内存大小
listen()方法是用来创建监听队列。 监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。 listen()第二个参数就是指定已完成三次握手队列的长度。
int listen(int sockfd,int backlog);
函数描述 服务端调用,将套接字由主动变为被动
参数说明
(1)sockfd
调用socket函数返回的文件描述符
(2)backlog
同时请求连接的最大个数(还未建立连接的客户端端口),最大为128,该参数意义不大,但不可以为0
返回值
成功返回0,失败返回-1并设置errno
accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
函数描述 服务端调用,从已连接队列中接受一个连接,并获得一个新的通信文件描述符
accept函数是一个阻塞函数,如果没有新的连接请求,就会一直阻塞
参数说明
(1)sockfd
调用socket函数返回的文件描述符
(2)addr
传出参数,保存客户端的地址信息
(3)addrlen
传入传出参数,addr变量所占内存空间大小
返回值
成功返回一个新的通信文件描述符,用于和客户端通信,失败返回-1, 并设置errno值.
connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7LYv9iKy-1667036779168)(C:\Users\Iric Zhang\AppData\Roaming\Typora\typora-user-images\image-20221029173512027.png)]
int connect(int sockfd, const struct socaddr* addr, socklen_t addrlen);
函数描述
客户端主动调用,连接服务器
参数说明
(1)sockfd
调用socket函数返回的文件描述符
(2)addr
服务端的地址信息
(3)addrlen
addr变量的内存大小
返回值
成功返回0,失败返回-1并设置errno
以上完成之后使用recv/send进行读写,读写数据又是也可以使用read/write,但两者都是配套使用
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//对应recv和send这两个函数flags直接填0
send()方法用来向 TCP 连接的对端发送数据。 send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。 send()的返回值为实际写入到发送缓冲区中的数据长度。
recv()方法用来接收 TCP 连接的对端发送来的数据。 recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
数据发送并接受完毕之后,使用close()方法用来关闭 TCP 连接。此时,会进行四次挥手。
代码测试过程中可以使用netstat进行查看监听状态和连接状态
服务端的开发流程
1.创建socket,返回一个文件描述符sockfd—socket(),该文件描述符用于监听客户端连接;
2.将sockfd和IP端口进行绑定----bind();
3.将sockfd由主动变为被动监听----listen();
4.接受一个新的连接,得到一个文件描述符c----accept(),该文件描述符是用于和客户端进行通信;
5.while(1)
{
接收数据—recv()
发送数据—send()
}
6.关闭监听和通信文件描述符----close(sockfd) close©;
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
//创建socket----int socket(int domain, int type,int protocal);
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
//绑定bind----int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in saddr, caddr,
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("192.168.31.143");
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
assert(res != -1);
//监听listen----int listen(int sockfd,int backlog);
listen(sockfd, 5);
while(1)
{
//接受连接accept----int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
int c = accept(sockfd,(struct sockaddr*)&caddr, &len);
printf("accept client ip:%s, port = %d\n",inet_ntoa(caddr.sin_addr), ntohs(caddr.sin+port));
if(c < 0)
{
continue;
}
printf("accept c = %d\n",c);
char buff[128];
//读数据
recv(c, buff, 127, 0);
printf("buff = %s\n",c);
//写数据
send(c, "ok", 2, 0);
//关闭监听
close(c);
}
//关闭通信描述符
close(sockfd);
exit(0);
}
测试工具:nc
客户端开发流程:
1.创建socket, 返回一个文件描述符sockfd—socket(),该文件描述符是用于和服务端通信;
2.连接服务端—connect() ;
3.while(1)
{
//发送数据—send
send写入发送缓冲区,内核负责从发送缓冲区拿出数据,网卡发送数据
//接收数据—recv
};
4.关闭通信描述符close(sockfd);
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
//创建socket
int sockfd = socket(AF_INET, SOCKET_STREAM, 0);
assert(sockfd != -1);
//连接服务器connect----int connect(int sockfd, const struct socaddr* addr, socklen_t addrlen);
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("192.168.31.143");
int res = connect(sockfd, (struct saddr*)&saddr, sizeof(saddr));
assert(res != -1);
//发送数据
printf("input:\n");
char buff[128];
fgets(buff, 127, stdin);
send(sockfd, buff, strlen(buff), 0);
//接收数据
memset(buff, 0, 128);
recv(sockfd, buff, 127, 0);
printf("read:%s\n",buff);
close(sockfd);
exit(0);
}