套接字-Socket | 爱编程的大丙 (subingwen.cn)
字节序
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。
目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian 和 Little-Endian(大端和小端)
-
Little-Endian -> 主机字节序 (小端)
- 数据的低位字节存储到内存的低地址位, 数据的高位字节存储到内存的高地址位
- 我们使用的PC机,数据的存储默认使用的是小端
-
Big-Endian -> 网络字节序 (大端)
- 据的低位字节存储到内存的高地址位, 数据的高位字节存储到内存的低地址位
- 套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。
1.转换函数
BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl
;从网络字节序到主机字节序的转换函数:ntohs、ntohl
。
#include <arpa/inet.h>
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int
// 这套api主要用于 网络通信过程中 IP 和 端口 的 转换
// 将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);
// 将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);
2.IP地址转换函数
虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);
-
参数
-
af
: 地址族(IP地址的家族包括ipv4和ipv6)协议 -
AF_INET
: ipv4格式的ip地址 -
AF_INET6
: ipv6格式的ip地址 -
src
: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100 -
dst
: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中
-
- 返回值:成功返回1,失败返回0或者-1
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
-
参数
-
af
: 地址族(IP地址的家族包括ipv4和ipv6)协议 -
AF_INET
: ipv4格式的ip地址 -
AF_INET6
: ipv6格式的ip地址 -
src
: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址 -
dst
: 传出参数, 存储转换得到的小端的点分十进制的IP地址
-
- 返回值:成功返回指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串。失败返回NULL
TCP通信流程
1.TCP通信特点
TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
- 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
- 安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
- 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致
2.服务器端通信流程
服务器端先启动。
-
创建用于监听的套接字, 这个套接字是一个文件描述符
int lfd = socket();
-
将得到的监听的文件描述符和本地的IP端口进行绑定
bind();
-
设置监听(成功之后开始监听, 监听的是客户端的连接)
listen();
-
等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的) ,没有新连接请求就阻塞
int cfd = accept();
-
通信,读写操作默认都是阻塞的
// 接收数据 read(); / recv(); // 发送数据 write(); / send();
-
断开连接, 关闭套接字
close(); // 服务器端和客户端各调用一次close()
3.客户端通信流程
-
创建一个通信的套接字
int cfd = socket();
-
连接服务器, 需要指定服务器的IP和端口
connect();
-
通信
// 接收数据 read(); / recv(); // 发送数据 write(); / send();
-
断开连接, 关闭文件描述符(套接字)
close();
套接字编程相关函数
1.socket()
man socket
:查看帮助文档
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/* 参数
domain:
AF_LOCAL->本地通信
AF_INET ->网络通信+IPv4
AF_INET6->网络通信+IPv6
type:指定传输层协议
SOCK_STREAM->流式传输协议,常用TCP
SOCK_DGRAM ->报式传输协议,常用UDP
protocol:
在type的时候指定为SOCK_STREAM,在此处值为0,则说明使用的是TCP
在type的时候指定为SOCK_DGRAM,在此处值为0,则说明使用的是UDP
*/
/* 返回值
成功:返回一个可以用于套接字通信的文件描述符。注意,这里的“通信”指既可以用于监听,也可以用于通信。
失败:返回-1
*/
2.bind()
man bind
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 参数
sockfd: 用于监听的文件描述符,用socket()创建可得
addr:存储的本地IP和端口信息。这里的IP和端口必须是”大端“的(网络字节序)
addrlen: 前一个参数addr占用内存的大小
*/
/* 返回值
成功:返回0
失败:返回-1
*/
3.sockaddr数据结构
// 在写数据的时候不好用
struct sockaddr {
sa_family_t sa_family; // 地址族协议,只能是ipv4。因为IPv6所用的结构体不一样
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_in
使用起来更简单
4.listen()
man listen
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/* 参数
sockfd: 用于监听的套接字,socket()创建并且用bind()进行了绑定
backlog:一次性能监听到多少个客户端的连接请求。最大值是128,128是在内存中写死了的。
注意,backlog限制的是“一次性”能监听到的连接请求,不代表服务器能和多少个客户端建立连接。
*/
/* 返回值
成功:返回0
失败:返回-1
*/
5.accept()
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 没有客户端发送连接请求时,这个函数是阻塞的。每调用一次只能和一个客户端建立连接。
/* 参数
sockfd: 用于监听的套接字
addr:客户端的IP和端口信息。传出参数
addrlen:前一个参数addr所占用内存的大小。传入传出参数
*/
/* 返回值
成功:返回一个用于通信的文件描述符
失败:返回-1
*/
6.connect()
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* 参数
sockfd: 客户端创建的用于通信的套接字
addr:指定想要连接的服务器端的IP和端口信息。
addrlen:前一个参数addr所占用内存的大小。
*/
/* 返回值
成功:连接(connection)或绑定(binding)成功,则返回0。
失败:返回-1
*/
// 客户端的套接字也需要绑定,connect()会帮套接字随机绑定一个空闲端口
7.read()/recv()
// 接收数据。两个函数头文件不同
#include <unistd.h>
ssize_t read(int sockfd, void *buf, size_t size);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
/* 参数
sockfd: 用于通信的文件描述符, accept()函数的返回值
buf:指向一块有效内存, 用于存储接收的数据
size:参数buf指向的内存的容量。防止溢出
flags: 特殊的属性,一般不使用,指定为 0
*/
/* 返回值
大于0:实际接收的字节数
等于0:对方断开了连接
-1:接收数据失败了
*/
8.write()/send()
// 发送数据的函数。两个函数头文件不同
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t len);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int fd, const void *buf, size_t len, int flags);
/* 参数
fd: 通信的文件描述符, accept() 函数的返回值
buf: 传入参数, 指向要发送的字符串
len: 要发送的字符串的长度
flags: 特殊的属性,一般不使用,指定为 0
*/
/* 返回值
大于0:实际发送的字节数,和参数len是相等的
-1:发送数据失败了
*/
9.close()
#include <unistd.h>
int close(int fd);
/* 参数
fd: 一个需要关闭的文件描述符(file descriptor)
*/
/* 返回值
成功:返回0
失败:返回-1
*/
文件描述符
在tcp的服务器端, 有两类文件描述符:
-
监听的文件描述符
- 只需要有一个
- 不负责和客户端通信, 负责检测客户端的连接请求, 检测到之后调用accept就可以建立新的连接
-
通信的文件描述符
- 负责和建立连接的客户端通信
- 如果有N个客户端和服务器建立了新的连接, 通信的文件描述符就有N个,每个客户端和服务器都对应一个通信的文件描述符
文件描述符在进行网络I/O操作时的对象:缓冲区
accept()
函数会检测监听文件描述符的读缓冲区
- 检测不到数据,该函数阻塞
- 如果检测到数据,解除阻塞, 新的连接建立
write()/send()
函数会将数据写入通信文件描述符对应的写缓冲区中
- 数据并没有被发送出去,只是被写入了缓冲区
- 当写缓冲区写被写满时,这俩函数会阻塞
- 内核检测到通信的文件描述符写缓冲区中有数据,,内核会将数据发送到网络中
read()/recv()
函数会检测通信文件描述符对应的读缓冲区
- 检测不到数据,该函数阻塞
- 如果检测到数据,解除阻塞, 并读出数据
服务器端的通信代码示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h> //包含了socket.h
int main()
{
// 1.创建监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
return -1;
}
// 2.绑定本地的IP和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(32456);
saddr.sin_addr.s_addr = INADDR_ANY; // 0=0.0.0.0。0不需要大小端转换。效果是,自动读取本地IP地址并绑定
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind");
return -1;
}
// 3.设置监听
ret = listen(fd, 128);
if(ret == -1){
perror("listen");
return -1;
}
// 4.等待客户端连接
struct sockaddr_in caddr;
int addrlen = sizeof(caddr);
int cfd = accept(fd, (struct sockaddr*)&caddr, &addrlen);
if(cfd == -1){
perror("accept");
return -1;
}
// 连接建立成功,打印客户端的IP和端口信息
char ip[24] = {0};
printf("客户端IP:%s,端口:%d\n",
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(caddr.sin_port));
// 5.通信
while(1){
//接收数据
char buff[1024];
int len = recv(cfd, buff, sizeof(buff), 0);
if(len >0){
printf("client say: %s\n",buff);
send(cfd, buff, len, 0);
}else if(len == 0){
printf("客户端已断开连接...\n");
break;
}else{
perror("recv");
break;
}
}
// 6.通信结束,关闭文件描述符
close(fd);
close(cfd);
return 0;
}
客户端的通信代码示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h> //包含了socket.h
int main()
{
// 1.创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
return -1;
}
// 2.连接服务器IP, port
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(32456);
inet_pton(AF_INET, "192.168.9.131", &saddr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1){
perror("connect");
return -1;
}
// 3.通信
int number = 0;
while(1){
// 发送数据
char buff[1024];
sprintf(buff, "你好, hello world, %d...\n", number++);
send(fd, buff, strlen(buff)+1, 0);
// 接收数据
memset(buff, 0, sizeof(buff));
int len = recv(fd, buff, sizeof(buff), 0);
if(len >0){
printf("server say: %s\n",buff);
}else if(len == 0){
printf("服务器已断开连接...\n");
break;
}else{
perror("recv");
break;
}
sleep(1);
}
// 4.通信结束,关闭文件描述符
close(fd);
return 0;
}