一.Socket通信基本介绍
Socket(套接字)是计算机网络中的一个重要概念,它指的是一种用于网络通信的编程接口。以下是关于Socket的详细解释:
1.定义与功能
定义:Socket是应用层与传输层之间的接口,它提供了一种标准的通信方式,使得不同的程序能够在网络上进行数据交换。Socket可以被视为网络通信的端点,它在网络上标识了一个通信链路的两端,并提供了通信双方所需的接口和功能。
功能:Socket允许应用程序通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。它允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。
2.工作原理
Socket的工作原理可以简单概括为以下几个步骤,如下图所示:
1.创建Socket:应用程序通过调用socket()函数创建一个Socket对象,并指定Socket的类型(如TCP或UDP)。
2.绑定地址和端口(仅服务器端):服务器端通过调用bind()函数将Socket对象与一个特定的IP地址和端口号绑定,这个端口就是服务器的标识,用于在网络上与其他主机建立连接。
3.监听连接(仅服务器端):服务器端通过调用listen()函数开始监听来自客户端的连接请求。
4.连接请求(客户端):客户端通过调用connect()函数向服务器发送连接请求,指定服务器的IP地址和端口号。
5.接受连接(服务器端):当服务器端监听到来自客户端的连接请求后,通过调用accept()函数接受连接请求,并返回一个新的Socket对象,用于与客户端进行通信。
6.数据传输:一旦建立连接,服务器和客户端就可以通过各自的Socket对象进行数据传输。它们通过读取和写入Socket对象上的数据流来发送和接收数据。
7.关闭连接:当通信完成或者出现错误时,可以通过关闭Socket对象来结束连接,释放资源。
3.类型
Socket主要分为两种类型:
1.TCP Socket:基于TCP协议,提供面向连接的、可靠的、字节流的服务。在TCP协议中,建立连接通常需要进行三次握手,以保证数据的可靠传输。
2.UDP Socket:基于UDP协议,提供无连接的、不可靠的、数据报的服务。UDP协议不保证数据的可靠性,但传输速度较快,适用于实时性要求较高的场景。
4.socket在本地进程间通讯应用
Socket(套接字)虽然主要用于不同主机之间的网络通信,但在同一主机上,进程间通信(IPC)也可以使用Socket,尤其是当需要模拟网络环境下的通信行为时。不过,在同一主机上进行进程间通信时,更常用的方式可能包括管道、消息队列、共享内存等。
二.相关函数
1.socket()函数
在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:
int socket(int af, int type, int protocol);
a.af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址。
b.type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
c.protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
2.bind()函数
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。函数的原型为:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
参数sock
是套接字的文件描述符,addr
是指向sockaddr
结构的指针,其中包含了要绑定的地址和端口信息,addrlen
是addr
结构的大小。示例代码如下,将创建的套接字与IP地址127.0.0.1、端口 1234 绑定:
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
3.connect()函数
主动发起一个连接请求,原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数sockfd
是套接字的文件描述符,addr
是指向sockaddr
结构的指针,其中包含了服务器的地址和端口信息,addrlen
是addr
结构的大小。
4.listen()函数
对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。函数原型为:
int listen(int sock, int backlog);
参数sock
是套接字的文件描述符,backlog
是指定的最大连接队列的长度。被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。
5.accept()函数
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
6.数据的接收和发送
Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。
write函数的原型为:
ssize_t write(int fd, const void *buf, size_t nbytes);
fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。
read函数的原型为:
ssize_t read(int fd, void *buf, size_t nbytes);
fd 为要读取的文件的描述符,buf 为要读取的数据的缓冲区地址,nbytes 为要读取的数据的字节数。read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。
recv()函数原型为:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
不论是客户端还是服务器都可以通过 revc()函数读取网络数据,它与 read()函数的功能是相似的。
参数sockfd
是套接字的文件描述符,buf
是指向数据缓冲区的指针,len
是要接收的数据长度,flags
是一些控制选项,如MSG_DONTWAIT
(非阻塞接收)、MSG_WAITALL
(等待接收完整数据)等。
send()函数原型为:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数sockfd
是套接字的文件描述符,buf
是指向数据缓冲区的指针,len
是要发送的数据长度,flags
是一些控制选项,如MSG_DONTWAIT
(非阻塞发送)等。
7.close()函数
当不再需要套接字描述符时,可调用 close() 函数来关闭套接字,释放相应的资源。
int close(int fd);
三.TCP Socket通信
1.服务器端
服务器端实现的功能有:创建TCP服务器,监听指定端口(8080),接受客户端连接,接收客户端消息并发送响应。代码如下所示:
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int sockfd, newsockfd, portno;
socklen_t clilen;
struct sockaddr_in serv_addr, cli_addr;
const int MAXDATASIZE = 1024;
char buffer[MAXDATASIZE];
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
std::cerr << "Error opening socket." << std::endl;
return 1;
}
// 设置服务器的地址和端口
portno = 8080;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
// 绑定套接字到地址和端口
if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
std::cerr << "Error on binding." << std::endl;
return 1;
}
// 开始监听
if (listen(sockfd, 5) < 0) {
std::cerr << "Error on listen." << std::endl;
return 1;
}
// 接受客户端连接
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr*)&cli_addr, &clilen);
if (newsockfd < 0) {
std::cerr << "Error on accept." << std::endl;
return 1;
}
// 接收消息
memset(buffer, 0, MAXDATASIZE);
ssize_t n = read(newsockfd, buffer, MAXDATASIZE - 1);
if (n == -1) {
std::cerr << "Error reading from socket." << std::endl;
return 1;
}
buffer[n] = '\0';
std::cout << "Message from client: " << buffer << std::endl;
// 发送响应
strcpy(buffer, "Message received.");
write(newsockfd, buffer, strlen(buffer));
// 关闭套接字
close(newsockfd);
close(sockfd);
return 0;
}
服务器端在main函数中的流程,如下所示:
创建socket (
AF_INET
表示IPv4,SOCK_STREAM
表示TCP)设置服务器地址结构(
sockaddr_in
)绑定socket到地址和端口
开始监听连接
接受客户端连接
接收客户端消息
发送响应
关闭连接
2.客户端
客户端实现的功能主要有:创建TCP客户端,连接到服务器(127.0.0.1:8080),发送消息到服务器,接收服务器响应。代码如下所示:
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int sockfd, portno;
struct sockaddr_in serv_addr;
const char* message = "Hello, server!";
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
std::cerr << "Error opening socket." << std::endl;
return 1;
}
// 设置服务器的地址和端口
portno = 8080;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(portno);
// 连接到服务器
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
std::cerr << "Error connecting." << std::endl;
close(sockfd);
return 1;
}
// 发送消息
send(sockfd, message, strlen(message), 0);
// 接收响应
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
recv(sockfd, buffer, sizeof(buffer) - 1, 0);
std::cout << "Response from server: " << buffer << std::endl;
// 关闭套接字
close(sockfd);
return 0;
}
客户端在main函数中的流程,如下所示:
创建socket
设置服务器地址结构
连接到服务器
发送消息
接收响应
关闭连接
3.总结及实现结果
上述通信流程总结为以下步骤;
首先运行服务器程序,它会:
绑定到8080端口
等待客户端连接
然后运行客户端程序,它会:
连接到服务器的8080端口
发送消息"Hello, server!"
等待服务器响应
通信过程:
服务器接收客户端消息并打印
服务器发送固定响应"Message received."
客户端接收并打印服务器响应
双方关闭连接
实现结果如下所示: