Linux学习--socket通信
学习心得
socket通信学习心得
socket通信的作用是什么?
为了实现不同主机之间的网络通信,Linux引进了socket通信
socket通信的过程是怎样的?从客户端和主机端两个方面阐述
从主机端开始说,必须先利用socket()函数建立一个socket套接字,然后定义一个socketaddr_in 结构体来保存主机端的IP地址和端口地址,再利用bing()函数命令将端口和IP和socket返回的文件描述符绑定。绑定后调用listen()函数创建监听等待队列。
之后在while循环中调用accept函数接受客户端发送来的连接请求,accept会返回一个新的文件描述符(监听套接字)来用于通信,连接成功后就可以调用send和recv来进行网络IO通信了(操作监听套接字)。
通常只会有1个监听套接字和多个连接套接字
再看客户端,同样利用socket()函数返回一个文件描述符,但客户端并不需要去bing()绑定一个端口IP,而是在connect()执行的时候系统就会自动分配,connect会向服务器发送连接请求,当连接成功就可以通过recv/send进行通信了。{当然可以通过sendto()/recvfrom直接省略connect的过程}
TCP/IP参考模型
关于TCP/IP协议
TCP/IP协议是一个复制的协议,是由一组专业化协议组成的。这些协议包括IP、TCP、UDP、ARP、ICMP以及其他的一些被称为子协议的协议。
各层次在模型中的作用
· 网络接口层
网络接口层是TCP/IP协议软件的最底层,负责将二进制流转换为数据帧,并进行数据帧的发送和接收。数据帧是网络传输的基本单元
网络层
网络层负责在主机之间的通信中选择数据报的传输路径,即路由。当网络层接收到传输层的请求后,传输某个具有目的地址信息的分组。该层把分组封装在IP数据报中,填入数据报的首部,使用路由算法来确定是直接交付数据报,还是把它传递给路由器,然后把数据报交给适当的网络接口进行传输。
网络层还要负责处理传入的数据报,检验其有效性,使用路由算法来决定应该对数据报进行本地处理还是应该转发。
如果数据报的目的机处于本机所在的网络,该层软件就会除去数据报的首部,再选择适当的运输层协议来处理这个分组。最后,网络层还要根据需要发出和接收ICMP(Internet控制报文协议)差错和控制报文。
传输层
传输层负责提供应用程序之间的通信服务。这种通信又称为端到端通信。传输层要系统地管理信息的流动,还要提供可靠的传输服务,以确保数据到达无差错、无乱序。为了达到这个目的,传输层协议软件要进行协商,让接收方回送确认信息及让发送方重发丢失的分组。传输层协议软件把要传输的数据流划分为分组,把每个分组连同目的地址交给网络层去发送。
应用层
应用层是分层模型的最高层,在这个最高层中,用户调用应用程序通过TCP/IP互联网来访问可行的服务。与各个传输层协议交互的应用程序负责接收和发送数据。每个应用程序选择适当的传输服务类型,把数据按照传输层的格式要求封装好向下层传输。
综上可知,TCP/IP分层模型每一层负责不同的通信功能,整体联动合作,就可以完成互联网的大部分传输要求。
TCP/IP模型的地址边界
TCP/IP分层模型中有两大边界特性:一个是地址边界特性,它将IP逻辑地址与底层网络的硬件地址分开;一个是操作系统边界特性,它将网络应用与协议软件分开,如图8.2所示。
TCP/IP分层模型边界特性是指在模型中存在一个地址上的边界,它将底层网络的物理地址与网络层的IP地址分开。该边界出现在网络层与网络接口层之间。
网络层和其上的各层均使用IP地址,网络接口层则使用物理地址,即底层网络设备的硬件地址。TCP/IP提供在两种地址之间进行映射的功能。划分地址边界的目的是为了屏蔽底层物理网络的地址细节,以便使互联网软件地址上易于实现和理解。
IP层特性
IP向上层提供统一的IP报文,使得各种网络帧或报文格式的差异性对高层协议不复存在。IP层是TCP/IP实现异构网互联最关键的一层。
TCP/IP的重要思想之一就是通过IP将各种底层网络技术统一起来,达到屏蔽底层细节,提供统一虚拟网的目的
TCP/IP的可靠性特性
TCP/IP的可靠性体现在传输层协议之一的TCP协议。TCP协议提供面向连接的服务,因为传输层是端到端的,所以TCP/IP的可靠性被称为端到端可靠性
综上可知,TCP/IP的特点就是将不同的底层物理网络、拓扑结构隐藏起来,向用户和应用程序提供通用、统一的网络服务。这样,从用户的角度看,整个TCP/IP互联网就是一个统一的整体,它独立于具体的各种物理网络技术,能够向用户提供一个通用的网络服务。
TCP/IP网络完全撇开了底层物理网络的特性,是一个高度抽象的概念,正是由于这个原因,其为TCP/IP网络赋予了巨大的灵活性和通用性。
TCP/IP协议族
TCP协议
TCP向应用层提供可靠的面向对象的数据流传输服务,TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递
应用程序通过向TCP层提交数据接发送/收端的地址和端口号而实现应用层的数据通
通过IP的源/目的可以惟一地区分网络中两个设备的连接;
通过socket的源/目的可以惟一地区分网络中两个应用程序的连接;
TCP三次握手四次释放
第一步(A->B):主机A向主机B发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号;
第二步(B->A):主机B在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示主机B的请求被接受,同时TCP序号被加一,ACK即确认
第三步(A->B):主机A也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成
超时重传机制
TCP实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报到达目的地后,接收方的TCP实体向回发送一个数据报,其中包含有一个确认序号,它意思是希望收到的下一个数据报的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据报。
TCP报头格式
UDP
UDP即用户数据报协议,是一种面向无连接的不可靠传输协议,不需要通过3次握手来建立一个连接,同时,一个UDP应用可同时作为应用的客户或服务器方。
由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。UDP比TCP协议更为高效,也能更好地解决实时性的问题,如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。
套接字定义
在Linux中的网络编程是通过socket接口来进行的。套接字(socket)是一种特殊的I/O接口,它也是一种文件描述符。socket是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。
常见的socket有3种类型如下。
(1)流式套接字(SOCK_STREAM)
流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的可靠性和顺序性。
(2)数据报套接字(SOCK_DGRAM)
数据报套接字定义了一种无可靠、面向无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。
(3)原始套接字(SOCK_RAW)
原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
地址和顺序处理
下面首先介绍两个重要的数据类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,如下所示:
两个结构体的效果是等效的,通常sockaddr_in更好用
struct sockaddr { unsigned short sa_family; /*地址族*/ char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号。*/ }; struct sockaddr_in { short int sa_family; /*地址族*/ unsigned short int sin_port; /*端口号*/ struct in_addr sin_addr; /*IP地址*/ unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/ };
sa_family可用参数
什么是网络字节序
网络字节序(Network Byte Order)是一种规定好的字节序,用于在不同计算机之间传输数据时保证数据的正确性。在网络字节序中,数据的字节序是固定的,即采用大端字节序(Big Endian)。
在网络通信中,为了保证数据的正确性,需要将数据转换为网络字节序后再传输
数据存储优先顺序
计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式,PC机通常采用小端模式)。
Internet上数据以高位字节优先顺序在网络上传输
因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了四个函数:htons()、ntohs()、htonl()和ntohl()。这四个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l代表long
头文件包含 #include <netinet/in.h>
uint16_t htons(unit16_t host16bit)
uint32_t htonl(unit32_t host32bit)
uint16_t ntohs(unit16_t net16bit)
uint32_t ntohs(unit32_t net32bit)
IP地址转换
用户在表达地址时通常采用点分十进制表示的数值字符串(或者是以冒号分开的十进制IPv6地址),而在通常使用的socket编程中所使用的则是二进制值(例如,用in_addr结构和in6_addr结构分别表示IPv4和IPv6中的网络地址),这就需要将这两个数值进行转换
这里在IPv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa()
inet_pton()函数是将点分十进制地址字符串转换为二进制地址(例如:将IPv4的地址字符串“192.168.1.123” 转换为4个字节的数据(从低字节起依次为192、168、1、123))
int inet_pton(int family, const char *strptr, void *addrptr) int inet_ntop(int family, void *addrptr, char *strptr, size_t len)
inet_ntop()是inet_pton()的反操向作,将二进制地址转换为点分十进制地址字符串
名字/地址的转化
在Linux中有一些函数可以实现主机名和地址的转化,如gethostbyname()、gethostbyaddr()和getaddrinfo()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。
其中
gethostbyname()是将主机名转化为IP地址;
gethostbyaddr()则是逆操作,是将IP地址转化为主机名;
另外getaddrinfo()还能实现自动识别IPv4地址和IPv6地址。
struct hostent *gethostbyname(const char *hostname)
涉及到的地址结构体
struct hostent { char *h_name; /*正式主机名*/ char **h_aliases; /*主机别名*/ int h_addrtype; /*地址类型*/ int h_length; /*地址字节长度*/ char **h_addr_list; /*指向IPv4或IPv6的地址指针数组*/ }
调用该函数时可以首先对hostent结构体中的h_addrtype和h_length进行设置,若为IPv4可设置为AF_INET和4;若为IPv6可设置为AF_INET6为6;若不设置则默认为IPv4地址类型
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **result)
在调用之前,首先要对hints服务线索进行设置。
struct addrinfo { int ai_flags; /*AI_PASSIVE, AI_CANONNAME;*/ int ai_family; /*地址族*/ int ai_socktype; /*socket类型*/ int ai_protocol; /*协议类型*/ size_t ai_addrlen; /*地址字节长度*/ char *ai_canonname; /*主机名*/ struct sockaddr *ai_addr; /*socket结构体*/ struct addrinfo *ai_next; /*下一个指针链表*/ }
addrinfo常见选项
#include <netdb.h>
1.1.1 套接字编程
socket编程的基本函数有socket()、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等,其中根据客户端还是服务端,或者根据使用TCP协议还是UDP协议,这些函数的调用流程都有所区别
各个函数的作用以及工作流程
socket()函数语法要点
#include <sys/socket.h> int socket(int family, int type, int protocol)
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
#include <sys/socket.h> int listen(int sockfd, int backlog)
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
accept函数是一个会阻塞的函数,在没有连接请求到来时会一直阻塞在此处,当有连接请求时阻塞解除并返回一个文件描述符,通过该文件描述符就可以和其它主机通信了。
#include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
#include <sys/socket.h> int send(int sockfd, const void *msg, int len, int flags)
#include <sys/socket.h> int recv(int sockfd, void *buf,int len, unsigned int flags)
该函数也是一个会阻塞的函数,
当连接建立没有断开时,接收端没有收到数据时recv会阻塞直到有数据接入 。
当连接断开了以后,没有数据时recv则不会阻塞而是直接返回0
#include <sys/socket.h> int sendto(int sockfd, const void *msg,int len, unsigned int flags, const struct sockaddr *to, int tolen)
#include <sys/socket.h> int recvfrom(int sockfd,void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen)
异步IO配合网络通信编程
内核通过使用异步I/O,在某一个进程需要处理的事件发生(例如,接收到新的连接请求)时,向该进程发送一个SIGIO信号。这样,应用程序不需要不停地等待着某些事件的发生,而可以往下运行,以完成其它的工作。只有收到从内核发来的SIGIO信号时,去处理它(例如,读取数据)就可以。
使用例
服务器端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
//1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) { //监听套接字创建失败
perror("socket");
return -1;
}
//2. 绑定本地的 IP : Port
/**
* 初始化 saddr 绑定 IP 和 Port 信息
*/
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; //IPv4
saddr.sin_port = htons(9999); //Port 需要转换成大端序
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)(&saddr), sizeof(saddr));
if (ret == -1) { //绑定失败
perror("bind");
return -1;
}
//3. 设置监听
ret = listen(lfd, 128);
if (ret == -1) { //监听失败
perror("listen");
return -1;
}
//4. 阻塞并等待客户端的连接
struct sockaddr_in caddr;
int caddr_len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr*)(&caddr), &caddr_len);
if (cfd == -1) { //连接失败
perror("accept");
return -1;
}
/**
*连接建立成功,打印客户端的 IP 和 Port 信息
注意:需要将信息由大端序 转为 小端序
*/
char ip[32];
printf("客户端的IP : %s , 端口Port : %d\n", inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(caddr.sin_port));
//5. 开始进行通信
char buf[1024];
while (1) {
//接受数据
int len = recv(cfd, buf, sizeof(buf), 0);
if (len > 0) { //还有数据
printf("Client say : %s\n", buf);
send(cfd, buf, sizeof(buf), 0);
}
else if (len == 0) { //说明客户端已经断开了连接
printf("客户端已经断开了连接...!\n");
break;
}
else { //len == -1 说明读取数据失败
perror("recv");
break;
}
}
//6.关闭文件描述符
close(lfd);
close(cfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
//1. 创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) { //监听套接字创建失败
perror("socket");
return -1;
}
//2. 连接服务器
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; //IPv4
saddr.sin_port = htons(9999); //Port 需要转换成大端序
inet_pton(AF_INET, "10.0.8.14", &saddr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)(&saddr), sizeof(saddr));
if (ret == -1) { //连接失败
perror("connect");
return -1;
}
//3. 开始进行通信
int num = 0;
while (1) {
char buf[1024];
//发送数据
sprintf(buf, "hello socket communication ... %d \n", num++);
send(fd, buf, strlen(buf) + 1, 0);
//接收数据
memset(buf, 0, sizeof buf);
int len = recv(fd, buf, sizeof(buf), 0);
if (len > 0) { //还有数据
printf("Server say : %s\n", buf);
}
else if (len == 0) { //说明服务端已经断开了连接
printf("服务端已经断开了连接...!\n");
break;
}
else { //len == -1 说明读取数据失败
perror("recv");
break;
}
sleep(1); // 每隔 1s 再发一次数据
}
//6.关闭文件描述符
close(fd);
return 0;
}
利用select实现多路IO复用
客户端
服务器端
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了