目录
1:Socket(套接字)之初步了解
套接字(Socket)是网络编程中一个非常基本的概念,它是网络通信过程中端点的抽象表示,允许程序之间的数据交换。在计算机网络中,套接字用于实现不同主机上的进程通信。
1.1套接字的概念
套接字是一种通信机制,它为进程间的通信提供了一个端点。在网络中,每个进程都可以通过一个唯一的地址来识别,这个地址由IP地址和端口号组成。因此,一个套接字通常由以下三部分组成:
协议族(通常为AF_INET,表示IPv4)
端口号(一个16位的数字,用于区分同一主机上运行的不同服务)
IP地址(用于标识主机)
1.2套接字的类型
套接字主要有两种类型:
流套接字(SOCK_STREAM):提供顺序、可靠、双向连接的字节流。流套接字使用传输控制协议(TCP)作为传输层协议,确保数据的可靠传输。
数据报套接字(SOCK_DGRAM):提供无连接的服务,数据以数据报的形式发送。每个数据报都是独立的,不保证顺序和可靠性。数据报套接字使用用户数据报协议(UDP)作为传输层协议。
1.3套接字的声明周期
一个套接字的生命周期通常包括以下几个步骤:
创建:使用socket()函数创建一个新的套接字。
绑定:使用bind()函数将套接字与一个地址和端口绑定,这样它就可以开始监听或接收数据。
监听(仅对服务器):服务器使用listen()函数监听来自客户端的连接请求。
接受(仅对服务器):服务器使用accept()函数接受客户端的连接请求,创建一个新的套接字用于与客户端通信。
连接(仅对客户端):客户端使用connect()函数发起对服务器的连接请求。
数据传输:使用send()和recv()(或read()和write())函数在客户端和服务器之间传输数据。
关闭:使用close()函数关闭套接字,释放资源。
1.4套接字的编程流程
1.4.1服务器端:
创建套接字。
绑定套接字到一个地址和端口。
监听端口上的连接请求。
接受客户端的连接请求。
使用接受的套接字进行数据传输。
关闭套接字。
1.4.2客户端:
创建套接字。
连接到服务器的地址和端口。
使用连接的套接字进行数据传输。
关闭套接字。
1.5套接字编程的API
在C和C++中,套接字编程通常使用以下系统调用:
socket():创建一个新的套接字。
bind():将套接字绑定到特定的IP地址和端口。
listen():使套接字进入监听状态,等待连接请求。
accept():接受一个连接请求,创建一个新的套接字用于通信。
connect():发起一个连接请求到服务器。
send() 和 recv():发送和接收数据。
close():关闭套接字。
1.6套接字编程的注意事项
错误处理:网络编程中,错误处理非常重要。每个系统调用都可能失败,需要检查返回值并适当处理错误。
异步处理:网络操作可能是异步的,需要考虑如何处理阻塞和非阻塞操作。
安全性:网络通信可能面临安全威胁,需要考虑加密和验证机制。
2.套接字编程之初体验
准备:这里我只用了一台Ubuntu虚拟机,Xshell(开两个窗口,一个服务器,一个客户端),VSCode(比较喜欢用VSCode连虚拟机写代码,直接Vim也行)。OK,开工!
2.1:服务端(server.cpp)
这里所有的解释就都写在注释了,应该算是写得详细了!
#include <iostream>
#include <cstring>
#include <sys/socket.h> //包含socket函数和数据结构
#include <netinet/in.h> //包含Internet地址簇
#include <unistd.h> //包含标准符号常数和类型
using namespace std;
const int PORT=1202;
int main(){
int server_fd,new_socket;//定义套接字文件描述符
/*server_fd表示服务器的文件描述符,在网络编程中,套接字(socket)通过文件描述符来操作,就像操作文件一样。
new_socket表示新的连接套接字,当服务器接受客户端的连接请求时,它会创建一个新的套接字来处理这个连接,
这个新的套接字通过 new_socket 变量来表示。*/
struct sockaddr_in address;//定义地址结构体
/*sockaddr_in 结构体是一个非常重要的数据结构,它用于存储网络地址信息。
这个结构体是 sockaddr 结构体的一个特化版本,专门用于IPv4地址。在 <netinet/in.h> 头文件中定义*/
int addrlen=sizeof(address);//地址长度
char buffer[1024]={0};//定义缓冲区
/*定义了一个大小为1024字节的字符数组,用于存储接收到的数据或将要发送的数据。{0} 初始化数组中的所有元素为0,
这是一个常见的做法,用于确保缓冲区不包含任何随机数据。当然了,它还有:保证数据完整性,提高效率,数据格式化,
防止数据丢失等功能,这里是简单示例,主要就是用于存储接收到的数据或将要发送的数据,就不做解释了,*/
const char *message="在想丽宝啊!";//定义服务器发送的消息,使用 const 可以防止函数意外修改字符串内容。安全
//创建套接字,用于在C++网络编程中创建一个TCP套接字的。
if((server_fd=socket(AF_INET,SOCK_STREAM,0))==0){
perror("socket failed");
exit(EXIT_FAILURE);
}
/*socket() 是一个系统调用,用于创建一个新的套接字。它返回一个文件描述符,
该文件描述符是用于后续套接字操作(如绑定、监听、连接等)的索引。
AF_INET表示IPv4地址族,用于创建一个基于IPv4的套接字。
SOCK_STREAM表示创建一个提供序列化、可靠、双向连接的字节流套接字,这通常用于TCP连接。
传递0表示使用默认的协议,对于AF_INET和SOCK_STREAM,这意味着使用TCP协议。
== 0检查server_fd是否为0,即检查套接字是否创建失败。
perror()函数将打印出错误信息到标准错误流
exit()函数用于终止当前程序,并返回一个状态码给操作系统。EXIT_FAILURE是一个宏,通常定义为非零值,表示程序异常终止
*/
//设置套接字选项,允许地址重用
int opt=1;
if(setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt))){
perror("setsockopt");
exit(EXIT_FAILURE);
}
/*opt这个变量用于表示套接字选项的状态,1 表示启用该选项。
setsockopt() 是一个系统调用,用于设置套接字选项。它可以改变套接字的行为,例如如何接收数据或者如何处理特殊的网络条件。
SOL_SOCKET是一个指定套接字选项级别的常量,表示这是针对套接字本身的选项,而不是针对某个特定协议的。
SO_REUSEADDR 是一个套接字选项,允许套接字绑定到一个已经被使用(在TIME_WAIT状态)的本地地址和端口。
SO_REUSEPORT 是一个扩展选项,允许多个套接字绑定到同一个端口,只要它们的地址不同。
如果 setsockopt() 返回非零值,表示设置选项失败。
*/
//绑定套接字到端口
address.sin_family=AF_INET;//地址簇,
address.sin_addr.s_addr=INADDR_ANY;//地址。INADDR_ANY表示接受任意IP的连接
address.sin_port=htons(PORT);//端口号,htons函数将主机字节序转换为网络字节序
/*这三段代码是设置服务器端套接字地址信息的一部分,它们配置了服务器将监听的协议族、IP地址和端口号
sin_family 是 sockaddr_in 结构体的一个成员,它指定了地址族。设置 sin_family 为 AF_INET 表示服务器将使用 IPv4 协议。
sin_addr 是 sockaddr_in 结构体的一个成员,它是一个 in_addr 结构体,用于存储 IPv4 地址。
INADDR_ANY 是一个特殊的常量,当绑定套接字时使用。它告诉操作系统自动绑定到所有可用的网络接口的 IPv4 地址。
sin_port 是 sockaddr_in 结构体的一个成员,用于指定端口号。htons() 函数是 "host to network short" 的缩写,它将一个短整型(16位)从主机字节序转换为网络字节序。
*/
//绑定套接字
if(bind(server_fd,(struct sockaddr *)&address,sizeof(address))<0){
perror("bind failed");
exit(EXIT_FAILURE);
}
/*bind() 是一个系统调用,用于将一个套接字(server_fd)绑定到指定的地址和端口上。
(struct sockaddr *)&address将address变量的地址传递给bind()函数
其他基本的东西都和上面提到过的大相径庭
*/
//监听套接字
if(listen(server_fd,3)<0){//监听,参数3表示最大连接数
perror("listen");
exit(EXIT_FAILURE);
}
/*listen() 是一个系统调用,用于告诉内核准备接受连接请求。这个函数将一个被动套接字(也称为监听套接字)转变为监听状态。
参数 3 表示服务器将允许最多3个连接请求在队列中等待接受。
*/
cout<<"Listening on port"<<PORT<<endl;
//接受客户端连接
if((new_socket=accept(server_fd,(struct sockaddr*)&address,(socklen_t*)&addrlen))<0){
perror("accept");
exit(EXIT_FAILURE);
}
/*accept() 是一个系统调用,用于从监听队列中取出第一个连接请求,并为该请求创建一个新的套接字。*/
//读取客户端发送的数据
read(new_socket,buffer,1024);//读取数据到缓冲区
cout<<"消息来自丽宝:"<<buffer<<endl;
/*read() 是一个系统调用,用于从文件描述符(在这里是套接字)读取数据。
new_socket是通过 accept() 函数创建的新套接字的文件描述符,用于与客户端进行通信*/
//发送数据回客户端
send(new_socket,message,strlen(message),0);//发送消息
cout<<"消息发送成功\n";
//关闭套接字
close(new_socket);//关闭客户端套接字
close(server_fd);//关闭服务器套接字
return 0;
}
2.2客户端(client.cpp)
客户端代码中有些在服务端代码中出现过,不清晰的看服务端代码解释就行
#include <iostream>
#include <sys/socket.h> // 包含socket函数和数据结构
#include <arpa/inet.h> // 包含inet函数,用于IP地址转换
#include <unistd.h> // 包含标准符号常数和类型
#include <string.h>
using namespace std;
const int PORT = 1202; // 定义服务器的端口号
int main() {
int sock = 0; // 定义套接字文件描述符
struct sockaddr_in serv_addr; // 定义服务器地址结构体
const char *hello = "赟赟在干嘛!"; // 定义客户端发送的消息
char buffer[1024] = {0}; // 定义接收数据的缓冲区
/*sock 用于存储客户端套接字的文件描述符,它是网络通信中的一个重要概念,代表了网络通信的一端。*/
// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
cout << "\n Socket creation error \n";
return -1;
}
/*如果创建成功,sock 将包含一个非负值;如果创建失败,则为 -1。*/
serv_addr.sin_family = AF_INET; // 服务器地址族
serv_addr.sin_port = htons(PORT); // 服务器端口号
// 将文本形式的IP地址转换为二进制形式
if (inet_pton(AF_INET, "192.168.47.128", &serv_addr.sin_addr) <= 0) {
cout << "\nInvalid address / Address not supported \n";
return -1;
}
/*inet_pton() 用于将表示网络地址的字符串转换为网络字节序的二进制形式。*/
// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
cout << "\nConnection Failed \n";
return -1;
}
/*connect() 用于客户端套接字与服务器套接字建立连接。*/
// 发送数据
send(sock, hello, strlen(hello), 0); // 发送消息
cout << "消息发送成功:\n";
// 接收服务器发送的数据
read(sock, buffer, 1024); // 从服务器接收数据
cout << "消息来自赟赟: " << buffer << std::endl;
// 关闭套接字
close(sock); // 关闭套接字
return 0;
}
3.测试
在XShell中先执行
g++ -o server server.cpp
g++ -o client client.cpp
选择一个窗口先运行服务器
./server
然后另一个窗口运行客户端
./client