传统的进程间通信借助内核提供IPC机制进行,但是只能限制于本机通信,若要进行跨机通信,就要使用网络通信。

​ 网络通信的本质是借助内核提供SOCKET伪文件的机制进行通信,实际上是使用了文件描述符,因此需要使用内核提供的socketAPI函数库(在传输层层面进行)。

使用socket会建立一个socket pair,如下图, 一个文件描述符操作两个缓冲区:内核中需要维护两个缓冲区,发送端缓冲区和接收端缓冲区, 这点跟管道是不同的, 管道是两个文件描述符操作一个内核缓冲区。

SOCKETPAIR

1.基础知识

主机字节序和网络字节序

主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。 在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。

大端:手机、网络 || 小端:电脑

主机字节序列:大端/小端 || 网络字节序列:大端

大小端转换

网络传输用的是大端法,如果机器用的是小端则需要进行大小端转换,Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:

#include <netinet/in.h>
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,如果本来不需要转换函数内部就不会转换。

通用socket地址结构

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) 的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:
sa_family 地址族变量

专用socket地址结构

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;//协议族
char sa_data[14];//数据,没有给出IP地址,就是给了这么一块儿空间,起了一个占位
的作用.
};

sa_family 成员是地址族类型(sa_family_t) 的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:
在这里插入图片描述

专用socket地址结构

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,尚处于试验阶段
};

IP地址转换函数

通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网
络字节序
char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符
串表示

函数名中p表示点分十进制的字符串形式,to表示到,n表示network

2.socket编程主要API函数(网络编程接口)

2.1socket函数

​ 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等。

2.2bind函数

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变量占用的内存大小

2.3listen函数

​ listen()方法是用来创建监听队列。 监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。 listen()第二个参数就是指定已完成三次握手队列的长度。

int listen(int sockfd,int backlog);

函数描述 服务端调用,将套接字由主动变为被动

参数说明

(1)sockfd

调用socket函数返回的文件描述符

(2)backlog

同时请求连接的最大个数(还未建立连接的客户端端口),最大为128,该参数意义不大,但不可以为0

返回值

成功返回0,失败返回-1并设置errno

2.4accept函数

​ 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值.
在这里插入图片描述

2.5connect函数

​ 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

2.6send、recv、close函数

以上完成之后使用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 连接。此时,会进行四次挥手。

在这里插入图片描述

2.7socketAPI编程流程图

在这里插入图片描述

代码测试过程中可以使用netstat进行查看监听状态和连接状态

3.代码实现

3.1服务端开发代码

服务端的开发流程

1.创建socket,返回一个文件描述符sockfd—socket(),该文件描述符用于监听客户端连接
2.将sockfd和IP端口进行绑定----bind();
3.将sockfd由主动变为被动监听----listen();
4.接受一个新的连接,得到一个文件描述符c----accept(),该文件描述符是用于和客户端进行通信;
5.while(1)
{
接收数据—recv()
发送数据—send()
}
6.关闭监听和通信文件描述符----close(sockfd) close©;

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>

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

3.2客户端开发代码

客户端开发流程:

1.创建socket, 返回一个文件描述符sockfd—socket(),该文件描述符是用于和服务端通信;
2.连接服务端—connect() ;
3.while(1)
{
//发送数据—send

send写入发送缓冲区,内核负责从发送缓冲区拿出数据,网卡发送数据

​ //接收数据—recv
​ };

4.关闭通信描述符close(sockfd);

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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);
}
Logo

更多推荐