【网络编程】socket套接字|sockaddr|sockaddr_in|通信过程
目录
编辑
源IP地址和目的IP地址
Socket
网络字节序
socket编程接口
sockaddr结构
sockaddr
sockaddr_in(IPv4)
sockaddr_in(IPv6)
完整的套接字(socket)通信过程
创建socket套接字
bind绑定套接字
listen建立监听
accept接受连接
connect建立连接
sendto发送数据
接收数据
close关闭套接字
源IP地址和目的IP地址
在网络中,IP地址标识了唯一一台主机,这表明只要我们知道了某个主机的IP地址,就可以向该主机发送数据;但是目标主机收到了数据后,不知道数据要具体发送给哪个进程。
为了解决这个问题,网络通信中引入了端口号的概念。端口号是一个16位的整数,它允许我们在同一台主机上区分不同的网络服务或应用程序。拿着IP地址和端口号就可以将数据发送给目标主机的具体某个进程;
网络通信本质还是进程间通信,只不过这两个进程可能不在一台主机上。
Socket
在进行网络通信时,客户端就相当于插头,服务端就相当于一个插座,但服务端上可能会有多个不同的服务进程(多个插孔),因此当我们在访问服务时需要指明服务进程的端口号(对应规格的插孔),才能享受对应服务进程的服务;
在网络中,两个进程想要互相通信,要有四个熟悉:源主机的IP+Port(端口号),目标主机的IP+Port(端口号)。IP+Port的组合就称为套接字Socket。
网络字节序
数据在内存中存储有大小端之分,而磁盘文件中的多字节数据相对于文件中的偏移量地址也有大小端之分,网络数据流也有大小端之分;
这样就引出了一个问题:如果通信的进程是在一台主机上还没啥问题(存储读取方式相同),如果是跨主机通信,一方是大端存储,一方是小端存储,假设不做处理发送给对方,不同的存储和读取方式肯定会引起乱码问题。
为了避免问题:需要将数据发送前转成网络字节序。而TCP/IP协议规定:网络数据流应采用大端字节序(低地址高地址),所以如果当前发送主机方是小端,需要先将数据转成大端;
- htonl函数的作用是将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送
- htons函数的作用是将 16 位的短整数从主机字节序转换为网络字节序,例如将 端口号 地址转换后准备发送
- ntohl函数的作用是将 32 位的长整数从网络字节序转换为主机字节序,例如将 接收到的 IP 地址转换后使用
- ntohs函数的作用是将 16 位的短整数从网络字节序转换为主机字节序,例如将接收到的端口号转换后使用
socket编程接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockaddr结构
sockaddr结构体之外还有两个结构体:sockaddr_in和sockaddr_un
- sockaddr_in结构体是用于跨网络通信,其中sockaddr_in的in是inet,"inet" 是Internet Protocol(IP)的简写
- sockaddr_un结构体是用于本地通信,其中sockaddr_un的un是unix
sockaddr 是一个通用的套接字地址结构,在不同的网络协议中被用来表示地址信息。这个结构通常用作更具体结构(如 sockaddr_in 用于IPv4, sockaddr_in6 用于IPv6)的基础。
sockaddr
struct sockaddr {
sa_family_t sa_family; // 地址族(Address family)
char sa_data[14]; // 套接字地址数据(Socket address data)
};
sockaddr_in(IPv4)
struct sockaddr_in {
sa_family_t sin_family; // 地址族(AF_INET)
in_port_t sin_port; // 端口号(Port number),网络字节序
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 填充字节,使结构体大小与 `sockaddr` 一致
};
sockaddr_in(IPv6)
struct sockaddr_in6 {
sa_family_t sin6_family; // 必须设置为 AF_INET6
in_port_t sin6_port; // 端口号,网络字节序
uint32_t sin6_flowinfo; // 流量控制信息
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 范围ID
};
struct in6_addr {
unsigned char s6_addr[16]; // 128位IPv6地址
};
完整的套接字(socket)通信过程
- 创建套接字:使用socket()函数来创建一个新的套接字。此函数需要三个参数:地址族(如AF_INET对应IPv4)、套接字类型(如SOCK_STREAM 表示TCP流套接字)和协议(一般设置为0,让系统选择默认协议)。
- 绑定地址:服务器端需要将套接字绑定到一个本地地址上,以便客户端可以连接到它。使用 bind() 函数来完成这个操作。首先定义一个 sockaddr_in 结构体,并填充相应的信息(IP地址和端口号),然后调用 bind()。
- 监听连接:对于服务器来说,下一步是监听传入的连接请求。使用listen() 函数,指定要监听的套接字和队列的最大长度。
- 接受连接:当有客户端尝试连接时,服务器通过 accept() 函数接受连接。这会创建一个新的套接字用于与客户端通信。
- 发送/接收数据:一旦连接建立,双方就可以通过 send() 和 recv() 函数或它们的变体(如 write() 和 read())交换数据了。
- 关闭连接:通信完成后,应该关闭套接字,释放资源。使用 close() 函数来关闭套接字。
创建socket套接字
#include <sys/types.h>
#include <sys/socket.h>
// 创建一个IPv4的TCP套接字
int socket(int domain, int type, int protocol);
- domain:地址簇,常见的有AF_INET(IPv4),和AF_INET6(IPv6)
- type:套接字类型,常见的有SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
- protocol:协议,通常为0(自动选择),也可以指定协议,如IPPROTO_TCP 或 IPPROTO_UDP。
- 成功时返回一个套接字描述符,失败时返回-1,并设置error
调用socket这个函数得到一个文件描述符,在操作系统的内核中,它对应于一个数据结构,存储了该套接字的各种状态信息和资源,例如IP地址、端口号、通信协议、缓冲区等。
bind绑定套接字
bind函数将套接字绑定到一个IP地址和端口号。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:套接字描述符
- addr:指向ockaddr结构体的指针,该结构体对象包含了要绑定的地址信息,对于IPv4,使用sockaddr_in 结构体;对于IPv6,使用 sockaddr_in6 结构体。
- addr_len:addr指向结构体对象的大小,通常使用sizeof获取
- 成功时返回0,失败时返回-1并设置errno
listen建立监听
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- sockfd:套接字描述符
- backlog
:
挂起连接的最大队列长度,即最多有多少个连接可以等待被接受- 成功时返回0,失败时返回-1并设置errno
accept接受连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:监听套接字描述符,即listen获取的套接字描述符
- addr:指向sockaddr结构体对象的指针,用于存储客户端地址信息
- addrlen:指向addr指向结构体对象的大小
- 成功时返回新的套接字描述符,失败时返回-1并设置errno
connect建立连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:套接字描述符
- addr:指向sockaddr结构体对象的指针,用于存储服务器地址信息
- addrlen:sockaddr 结构体的大小。
- 成功时返回0,失败时返回-1并设置errno
sendto发送数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
- sockfd: 套接字描述符,通过 socket 函数创建
- buf: 指向要发送的数据缓冲区
- len: 要发送的数据的长度
- flags: 发送标志,通常为0
- dest_add: 指向 sockaddr 结构体的指针,包含目标地址和端口号
- addrlen: sockaddr 结构体的大小
- 成功时返回发送的字节数,失败时返回-1,并设置 errno 以指示错误
接收数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- sockfd: 套接字描述符,通过 socket 函数创建。
- buf: 指向存储接收数据的缓冲区。
- len: 缓冲区的长度,即可以接收的最大字节数。
- flags: 接收标志,通常为0。
- src_addr: 指向 sockaddr 结构体的指针,用于存储发送方的地址信息。
- addrlen: 指向 socklen_t 变量的指针,表示 sockaddr 结构体的大小。调用函数时需要设置为 sockaddr 结构体的大小,函数返回时设置为实际地址的长度。
close关闭套接字
#include<unistd.h>
int close(int fd);