【Linux网络】——Socket网络编程
前言
在当今数字化的时代,网络通信已经成为计算机领域不可或缺的一部分。无论是日常的网页浏览、社交媒体交互,还是大规模的企业级数据传输,都离不开高效可靠的网络通信。而在Linux操作系统中,Socket网络编程是实现各种网络应用的基础技术。通过使用Socket编程,开发人员可以在不同的计算机之间建立连接,实现数据的发送和接收,为各种网络应用程序的开发和部署提供了强大的支持。
Socket网络编程
基本概念
Socket是一种用于在网络中传输数据的抽象接口。它定义了应用程序与网络操作系统之间的通信接口,允许应用程序通过网络进行数据交换。Socket可以看作是一个通信的端点,包含了IP地址和端口号。
ip地址
- 标识网络中设备唯一身份的数字标签。
- 在IPv4中,IP地址由32位二进制数表示,通常以点分十进制表示法表示,如192.168.1.1。而在IPv6中,IP地址由128位二进制数表示,采用冒号十六进制表示法。
端口号
- 端口号用于标识网络应用程序在同一台计算机上的不同连接。
- 端口号由16位无符号整数表示,范围从0到65535。端口0到1023是系统保留端口,通常用于特定的服务,如HTTP(80)、FTP(21)等。自定义应用程序通常使用1024以上的端口号。
IP地址和端口号互相配合,IP地址负责找到唯一的一台网络设备,端口号负责找到这台设备上的唯一连接,就组成了唯一的一个地址,让数据有了起点和终点
Socket网络编程接口
理解了Socket编程所需的基本概念,接下来,就需要看看其接口
创建套接字——socket
在开始网络通信之前,需要创建一个套接字。
在Linux中,通过调用socket()
系统调用来创建套接字。
函数造型:
int socket(int domain, int type, int protocol);
参数说明:
domain
:地址族,常用的有AF_INET
(IPv4)、AF_INET6
(IPv6)。type
:套接字类型,如SOCK_STREAM(TCP协议)
、SOCK_DGRAM(UDP协议)
。protocol
:协议类型,通常设为0,表示默认协议。
返回值说明:
- 成功时返回一个非负整数,表示Socket描述符。
- 失败时返回
-1
,并设置errno
。
绑定套接字——bind
将Socket绑定到一个特定的IP地址和端口。
绑定地址后,该套接字相当于在网络中有了唯一性,这些必不可少的一环
函数造型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
sockfd
: Socket描述符。addr
: 指向包含IP地址和端口信息的sockaddr
结构体。addrlen
:addr
结构体的大小。
返回值说明
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
。
sockaddr结构体
单独将sockaddr结构体拎出来讲一下
在Linux的Socket编程中,sockaddr
结构体是一个通用的地址结构体,用于表示网络地址信息。
它的基本形式如下:
struct sockaddr {
sa_family_t sa_family; // 地址族 (Address family)
char sa_data[14]; // 协议地址 (Protocol address)
};
成员说明:
-
sa_family
: 地址族,表示地址的类型。常用的值包括:AF_INET
: IPv4 地址族AF_INET6
: IPv6 地址族AF_UNIX
: Unix 域套接字(用于本地进程间通信)- 其他地址族(如
AF_PACKET
用于原始套接字等)
-
sa_data
: 这是一个14字节的数组,用于存储具体的协议地址信息。对于不同的地址族,sa_data
中存储的内容不同。例如,对于AF_INET
,sa_data
包含IP地址和端口号。
不过,sockaddr
是一个通用的结构体,实际使用时通常会使用更具体的结构体,如 sockaddr_in
(用于IPv4)或 sockaddr_in6
(用于IPv6)。
sockaddr_in
结构体(用于IPv4)
在实际编程中,sockaddr_in
结构体更常用于表示IPv4地址信息。它是对 sockaddr
的扩展,提供了更明确的字段来表示IPv4地址和端口。
基本样式
struct sockaddr_in {
sa_family_t sin_family; // 地址族,必须是 AF_INET
in_port_t sin_port; // 端口号,使用网络字节序(大端)
struct in_addr sin_addr; // IP地址,使用网络字节序
char sin_zero[8]; // 填充字段,与 `sockaddr` 的大小对齐
};
成员说明:
-
sin_family
: 地址族,必须设置为AF_INET
。 -
sin_port
: 端口号,使用网络字节序(大端)。可以使用htons()
函数将主机字节序转换为网络字节序。 -
sin_addr
: IP地址,使用in_addr
结构体表示。in_addr
结构体内部只有一个成员s_addr用来存储IP地址
-
sin_zero
: 这是一个8字节的填充字段,用于与sockaddr
结构体的大小对齐。通常设置为0。
sockaddr_in6
结构体(用于IPv6)
对于IPv6地址,可以使用 sockaddr_in6
结构体
基本样式:
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,必须是 AF_INET6
in_port_t sin6_port; // 端口号,使用网络字节序
uint32_t sin6_flowinfo; // IPv6流信息
struct in6_addr sin6_addr; // IPv6地址
uint32_t sin6_scope_id; // 作用域ID(用于链路本地地址)
};
关于sockaddr_in6我们不过多介绍,因为实际socket中基本不使用,很难见到
监听套接字——listen
将Socket设置为监听状态,等待客户端的连接请求。
基本样式
int listen(int sockfd, int backlog);
参数说明
sockfd
: Socket描述符。backlog
: 指定等待连接队列的最大长度。
返回值说明
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
。
之前进行三次握手的时候说过,会有一个连接队列,用于暂时保存等待的连接
这里的listen就可以设置改连接队列的大小,指定等待连接的数量
接受连接——accept
接受客户端的连接请求,并返回一个新的Socket描述符用于与客户端通信。
函数造型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
sockfd
: 监听Socket描述符。addr
: 指向存储客户端地址信息的sockaddr
结构体。addrlen
: 指向addr
结构体大小的指针。
返回值说明:
- 成功时返回一个新的Socket描述符,用于与客户端通信。
- 失败时返回
-1
,并设置errno
。
请求连接——coonect
客户端使用该函数连接到服务器。
函数造型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd
: Socket描述符。addr
: 指向包含服务器IP地址和端口信息的sockaddr
结构体。addrlen
:addr
结构体的大小。
返回值说明:
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
。
发送与接受——send 和 recv
send()
用于发送数据,recv()
用于接收数据。
两者均需要使用被绑定过,且已被连接的套接字来捕获数据
基本样式
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明
sockfd
: Socket描述符。buf
: 指向发送或接收数据的缓冲区。len
: 数据的长度。flags
: 通常为0,表示阻塞模式。
注意:
使用socket套接字进行网络通信以前,必须对套接字进行绑定和连接建立!否则无法发送或者接收数据
socket网络通信
服务端网络编程
了解完接口,接下来尝试使用socket套接字完成服务器的搭建
服务器搭建一般流程:
创建套接字
绑定套接字
监听连接
接受连接
发送与接受
关闭套接字
1.创建套接字
//创建套接字
int socket_fd = socket(AF_INET,SOCK_STREAM,0);
2.绑定套接字
//先创建一个struct sockaddr_in结构体对象并填充
struct sockaddr_in serv_addr;
//将内部清零
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY表示监听所有的接口
serv_addr.sin_port = htons(8080); // 填充端口号
bind(socket_fd,(sockaddr*)(&serv_addr),sizeof(serv_addr)
注意:
- 我们这边并没有将服务端的ip地址设置成唯一的,而是使用INADDR_ANY,是表示监听所有ip地址的连接,因为一台网络设备不一定只有一个ip地址
3.监听套接字
int length = 1;
if(listen(socket_fd,length)!=0){
perror("listen failed");
close(socket_fd);
exit(EXIT_FAILURE);
}
注意:
- 这个等待队列是可以进行根据自己的需求进行设置
4.接收连接
//连接套接字
struct sockaddr_in cilent_addr;
memset(&cilent_addr,0,sizeof(cilent_addr));
socklen_t cilent_len = sizeof(cilent_addr);
int connect_fd = accept(socket_fd,(sockaddr*)(&cilent_addr),&cilent_len);
5.发送与接收
while(flag){
char recv_buff[1024];
//这里用于发送数据和接受数据
recv(connect_fd,recv_buff,sizeof(recv_buff),0);
std::cout<<"cilent say:"<<recv_buff<<std::endl;
char send_buff[1024] = "server have got it!";
send(connect_fd,send_buff,sizeof(send_buff),0);
}
6.关闭连接
close(socket_fd);
close(connect_fd);
客户端网络编程
客户端搭建一般流程:
创建套接字
设置服务器信息
请求连接
数据发送与接收
关闭连接
1.创建套接字
//创建套接字
int socket_fd = socket(AF_INET,SOCK_STREAM,0);
2.设置服务器信息
//设置服务器地址结构
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
inet_pton(AF_INET,server_ip.c_str(),&server_addr.sin_addr);
3.请求连接
if(connect(socket_fd,(sockaddr*)(&server_addr),sizeof(server_addr))!= 0){
perror("connect failed");
close(socket_fd);
exit(EXIT_FAILURE);
}
注意:
- 客户端并不需要自己设置自己的ip地址和端口,一旦连接建立成功,发送数据,这些都会自己绑定
客户端的ip地址是唯一的,但端口号不唯一,而是操作系统随机分配的
为什么端口号要随机分配?
如果一个主机起了多个客户端,这些客户端都会进行抢占端口
如果不进行随机绑定,而是特定端口的话
就会出现两个客户端同时使用一个端口的情况,导致其中一个客户端无法通信
例如
- 夸克使用8080端口,网易云也使用8080端口
- 夸克客户端先启动,就会先占用这个端口,网易云就不能再使用这个端口,无法进行通信
因为一个端口只能一个进程,另一个进程就无法通信
4.数据的发生与接收
//接受和发送数据
while(flag){
char recv_buff[1024];
char send_buff[1024] = "hello ,i am cilent";
send(socket_fd,send_buff,sizeof(send_buff),0);
//这里用于发送数据和接受数据
recv(socket_fd,recv_buff,sizeof(recv_buff),0);
std::cout<<"server say:"<<recv_buff<<std::endl;
}
5.关闭连接
//关闭套接字
close(socket_fd);
通信示例
启动服务端
./server
启动客户端
./cilent
运行结果:
服务端
客户端
其实网络通信也是进程通信的一种
不过由原来的主机之间不同进程通信,变成了不同主机间的进程通信。
结语
Socket编程的学习是一个不断深入的过程,每一次的实践都是对网络通信理解的深化。希望本文能够为你在Socket编程的学习道路上提供一些帮助,并激发你对网络编程的兴趣和探索欲望。
在未来的学习和实践中,愿你能够不断挑战自我,掌握更多的网络编程技能,创造出更加出色的网络应用程序!无论你是继续深入研究网络通信的底层原理,还是专注于开发高性能的网络服务,Socket编程都将是你不可或缺的工具和基石。编程之路虽长,但每一步都充满了成长的机会和无限的可能。加油!