1、Linux中的socket与TCP通信
时间:2022-09-12 22:30:01
socket介绍
1、所谓 socket(套接字),就是抽象端点在网络中不同主机的应用过程之间进行双向通信
。
2.套接字是网络上流程通信的一端,为应用层流程利用网络协议交换数据提供了机制。从位置上看,套接字上联应用程序和下联网络协议栈是应用程序通过网络协议通信的接口,是应用程序与网络协议根交互的接口。
3、socket 可以看作是两个网络应用程序通信时通信连接的端点,这是一个逻辑概念
。在网络环境中进程间通信 API,也是可以命名和搜索的通信端点,使用中的每个套接字都有自己的类型和连接过程。其中一个网络应用程序将要传输的信息写入其主机 socket 中,该 socket 通过网络接口卡(NIC)连接的传输介质将此信息发送给另一台主机 socket 中间,让对方收到这个信息。socket 是由 IP 结合地址和端口,提供将数据包传输到应用层过程的机制。
4、在 Linux 环境下,socket用于表示过程中网络通信的特殊文件类型。本质是内核借助缓冲区形成的伪文件
。既然是文件,自然可以用文件描述符引用套接字。类似于管道,Linux 系统将其包装成文件的目的是统一接口,使读写连接与读写文件的操作一致。不同之处在于,管道主要用于本地过程间通信,而连接字主要用于网络过程间数据的传输。
5、socket套接字通信分为两部分:
* 服务器端:被动接受连接,一般不主动启动连接
* 客户端:主动连接服务器
socket是一套通信接口,Linux 和 Windows 但是有一些细微的差别。
字节序
顾名思义,字节序是字节的顺序,大于字节类型数据在内存中的存储顺序。如果数据小于或等于字节,则不考虑此问题。
简介
1、现代CPU累加器一次至少可以装载4字节,即整数。 4字节在内存中的顺序会影响累加器装载的整数值,这就是字节序的问题(类似于现代人从左到右写字,古人从右到左写字)。
2.在各种计算机系统结构中,字节和单词的存储机制不同,导致计算机通信领域的一个非常重要的问题,即通信双方交流的信息单元(比特、字节、单词、双字等)应按什么顺序传输。如果不一致,通信将失败。
3.字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指整数的最高字节(23 ~ 31 bit)存储在内存的低地址,低字节(0 ~ 7 bit)存储在内存中的高地址
;小端字节序是指存储在内存高地址的整数高字节,而存储在内存低地址的低字节
。小端字节序符合我们的常规思维,很多电脑也使用小端字节序。大端字节序统一用于网络。
转换函数的字节序
1.当格式化数据直接传输到两个使用不同字节序的主机之间时,接收端必须错误解释。解决问题的方法是:发送器总是将要发送的数据转换为大端字节序数据,接收器知道对方发送的数据总是使用大端字节序,因此接收器可以根据自己的字节序决定是否转换接收到的数据(小端转换,大端不转换)
。
2.网络字节顺序是 TCP/IP 一种具体的数据表示格式 CPU 类型、操作系统等输数据时,类型、操作系统等之间传输时能够正确解释,网络字节顺序采用大端排序方式
。
3、BSD Socket为程序员提供包装好的转换接口。包括从主机字节目到网络字节目的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
字节序函数转换函数 #include /* 函数名分解: h - host 主机,主机字节序 to - 转换成什么 n - network 网络字节序 s - short unsigned short(2个字节) l - long unsigned int (4个字节) */ ///主机字节序 --> 网络字节序 uint32_t htonl(uint32_t hostlong); //转ip地址 uint16_t htons(uint16_t hostshort); //转端口port //网络字节序 --> 主机字节序 uint32_t ntohl(uint32_t netlong); //转ip地址 uint16_t ntohs(uint16_t netshort); //转端口port
说明
:为什么以上两个函数只说地址和端口,而不是实际传输的数据?
a、端口号和地址需要字节序转换:是因为TCP/IP协议栈要求的,必须转。
b、数据不需要字节序转换:不是真的不需要转换,因为我们现在使用它PC机器,它们的主机字节序是相同的(小端),所以即使我们的数据在网络传输过程中没有字节序转换,对方也可以在收到后正确存储。如果收到大端主机,收到中文(两个字节数据)就会出错。为了保证两个主机都能正通信,在传输过程中必须转换字节序。(注:一个字节的数据(如单个字符)传输不需要字节转换)
使用
:在socket例如,如果客户端需要连接到服务器,则需要包装服务器的地址和端口socket在地址中,这两个数据需要转换到网络字节序。通常是转移端口,ip地址必须转换为网络可接受的数据类型,因此有其他函数可以处理(包括转字节序的功能)。
转换函数示例
#include #include int main() {
// htons 转换端口 unsigned short a = 0x0102; printf("a : %x\n", a); unsigned short b = htons(a);
printf("b : %x\n", b);
printf("=======================\n");
// htonl 转换IP
char buf[4] = {
192, 168, 1, 100};
int num = *(int *)buf;
int sum = htonl(num);
unsigned char *p = (char *)∑
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
printf("=======================\n");
// ntohl
unsigned char buf1[4] = {
1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char *p1 = (unsigned char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
return 0;
}
socket地址
简介
socket地址其实是一个结构体,封装端口号和IP等信息。在socket编程中就需要使用该socket地址。
通用socket地址
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr
,其定义如下:
#include
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
typedef unsigned short int sa_family_t;
结构体成员介绍:
sa_family
成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data
成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值
。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#include
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;
专用socket地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
1、UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include
struct sockaddr_un
{
sa_family_t sin_family;
char sun_path[108];
};
2、TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和IPv6:
#include
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr; //uint32_t
};
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
//别名
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
注意:所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr
(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是sockaddr。
通常,在设置socket地址具体数据的时候都是用专用地址(ipv4居多),作为参数传递进入socket函数的时候就强转为通用sockaddr类型。
IP地址转换
简介
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用
。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串
。下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:
#include
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址,下面两函数更加常用:
会对其进行字节序转换。
#include
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
实例
int main()
{
// int inet_pton(int af, const char *src, void *dst);
//点分十进制 --> 网络字节序
char *src = "192.168.43.221";
void *dst = malloc(sizeof(void *));
// unsigned short int dst = 0;
int ret = inet_pton(AF_INET, src, dst);
perror("ret = ");
unsigned char *dstV = (unsigned char *)dst;
printf("%d %d %d %d\n", dstV[0], dstV[1], dstV[2], dstV[3]);
char dst2[16];
inet_ntop(AF_INET, dst, dst2, 16); // 表示dst2的长度
printf("%s\n", dst2);
}
TCP通信流程
简介
TCP 和 UDP -> 传输层的协议
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接
// 客户端
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接
套接字相关函数
#include
#include
#include // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1,并且设置对应的错误编号
-该函数相当有获取了一个文件描述符。之后的操作都依赖于这个文件描述符。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定。
- 描述:
当使用 socket(2) 创建套接字时,它存在于名称空间(地址族)中,但没有分配给它的地址。
bind() 将 addr 指定的地址分配给文件描述符 sockfd 引用的套接字。
addrlen 指定 addr 指向的地址结构体的大小(以字节为单位)。 传统上,此操作称为“为套接字分配名称”。
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
- 返回值:
- 成功: 返回 0
- 失败: 返回 -1, 并且设置对应的错误编号
int listen(int sockfd, int backlog); // 配置文件: /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接(将sockfd套接字指定为被动套接字)
- 描述:
listen() 将 sockfd 引用的套接字标记为被动套接字,即将使用 accept(2) 接受传入连接请求的套接字。
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值 --> 系统设定了一个最大值,在/proc/sys/net/core/somaxconn有设定
使用者可以设置一个不大于系统设置的值
- 返回值:
- 成功: 返回 0
- 失败: 返回 -1, 并且设置对应的错误编号
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符,之后的通信就依赖于这个文件描述符
- 失败 : 返回 -1, 并且设置对应的错误编号
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1, 并且设置对应的错误编号
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
实例1、客户端与服务端通信
客户端输入消息(用户通过键盘录入消息),服务端回应。
服务端
代码
#include
#include
#include
#include
#include
// 根据用户端在键盘中输入的信息作为传输数据
int main()
{
// 1、创建socket,返回文件描述符,是一个用于监听的套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd == -1)
{
perror("socket");
exit(-1); //失败直接结束进程
}
// 2、绑定ip和端口等数据(服务器端自身的特征数据)
struct sockaddr_in addr; //先使用专用网络地址封装然后强转即可
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); //需要将主机字节序转换为网络字节序
//inet_pton(AF_INET, "192.168.47.131", &(addr.sin_addr.s_addr)); //网络中需要将点分十进制的地址转换为网络中用的整数
addr.sin_addr.s_addr = INADDR_ANY; //一个电脑可能有多个网卡,如果使用这个宏,那么客户端连接任意一个网卡的ip地址都可以链接到服务端。这个宏相当于0
int ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); //第二个参数需要强转成通用结构
if (ret == -1)
{
perror("bind");
exit(-1);
}
// 3、监听,处于等待客户端连接的阻塞状态
ret = listen(sfd, 6);
if (ret == -1)
{
perror("listen");
exit(-1);
}
// 4、接受客户端连接
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
int client_fd = accept(sfd, (struct sockaddr *)&client_addr, &len);
// 4.1 输出客户端的信息 ip和port (此时又需要将网络字节序转换为主机字节序)
char client_ip[16];
inet_ntop(AF_INET, &(client_addr.sin_addr.s_addr), client_ip, 16);
uint16_t client_port = ntohs(client_addr.sin_port); //将网络字节序的端口转到本机字节序
printf("%s %d\n", client_ip, client_port);
// 5、通信开始
char recvBuf[1024] = {
0};
while (1)
{
//获取客户端数据
int num = read(client_fd, recvBuf, sizeof(recvBuf));
if (num == -1)
{
perror("read");
exit(-1);
}
else if (num > 0)
{
printf("Server收到的消息 --> %s\n", recvBuf);
}
else if (num == 0)
{
//客户端断开连接
printf("Client close...\n");
break;
}
char *data = "接收到用户数据!";
//向客户端发送数据
write(client_fd, data, strlen(data));
}
//关闭文件描述符
close(client_fd);
close(sfd);
return 0;
}
客户端
代码
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
// 1、创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");
exit(-1);
}
//连接到服务器(这里封装的数据都是服务器的ip和端口)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.47.131", &(addr.sin_addr.s_addr));
addr.sin_port = htons(8888);
int ret = connect