服务端
创建流程
一、 调用socket函数创建监听socket
socket
套接字:表示通信的端点。就像用电话通信,套接字相当于电话,IP地址相当于总机号码,而端口号则相当于分机号码。
int socket(int domain, int type, int protocol);
1. domain(协议族):
指定通信的协议族,常见的有:
- AF_INET:IPv4 协议。
- AF_INET6:IPv6 协议。
- AF_LOCAL 或 AF_UNIX:本地通信(同一台计算机上的进程间通信)。
- AF_PACKET:底层网络通信,允许访问物理层,如以太网帧。
2. type(套接字类型):
指定套接字的类型,常见的有:
- SOCK_STREAM:面向连接的字节流套接字(TCP),保证数据的顺序和可靠传输。
- SOCK_DGRAM:无连接的数据报套接字(UDP),不保证数据的顺序和可靠性。
- SOCK_SEQPACKET:有序数据包套接字(面向消息的字节流套接字)。
- SOCK_RAW:原始套接字,用于操作网络层协议(如 ICMP、IP)。
最常见的选项是 SOCK_STREAM 和 SOCK_DGRAM。
3. protocol(协议):
- 指定使用的具体协议,通常设置为 0,让系统选择默认协议:
- 当 type 为 SOCK_STREAM 时,默认是 TCP 协议。
- 当 type 为 SOCK_DGRAM 时,默认是 UDP 协议。
函数返回值
成功时,返回一个非负的文件描述符(Socket 描述符),用于后续的网络通信。
失败时,返回 -1,并设置 errno 以指示错误原因。
所以创建一个socket:
// 创建一个监听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
//常见的AF_INET──指定为IPv4协议,AF_INET6──指定为IPv6,AF_LOCAL──指定为UNIX 协议域
//套接口可能的类型有:SOCK_STREAM字节流、SOCK_DGRAM数据报、SOCK_SEQPACKET有序分组、SOCK_RAW原始套接口
//传输协议TCP/UDP,这里默认0
if (listenfd == -1) {
cout << " create listen socket error " << endl;
return -1;
}
二、创建struct sockaddr_in结构体,并调用bind函数
struct sockaddr_in
是一种用于存储 IPv4 地址信息的结构体,在网络编程中用于指定套接字的地址信息,例如绑定地址、连接地址等。这个结构体是sockaddr
的特化版本,专门用于 IPv4 协议。它通常与bind()、connect()、accept()
等函数一起使用。
struct sockaddr_in
在 <netinet/in.h> 头文件中定义,通常如下:
struct sockaddr_in {
sa_family_t sin_family; // 地址族(Address Family)
in_port_t sin_port; // 端口号(Port Number)
struct in_addr sin_addr; // IP 地址(Internet Address)
char sin_zero[8]; // 填充字段(Padding, 保持结构体与 sockaddr 的长度一致)
};
其中的参数定义如下:
1. sin_family:
+ 类型:sa_family_t
+ 说明:指定地址族,必须设置为 AF_INET,表示使用 IPv4 协议。
2. sin_port:
- 类型:in_port_t(通常是 uint16_t)
- 说明:指定端口号,端口号使用网络字节序(大端字节序)。在赋值时,需使用 htons() 函数将主机字节序转换为网络字节序。例如,端口号 8080 设置为:sin_port = htons(8080);
3. sin_addr:
-
类型:struct in_addr
-
说明:用于存储 IPv4 地址,同样需要使用网络字节序。
-
struct in_addr 的定义如下:
struct in_addr { uint32_t s_addr; // 32 位 IPv4 地址 };
-
常见赋值方式:
sin_addr.s_addr = inet_addr("127.0.0.1"); // 使用字符串表示的 IP 地址 sin_addr.s_addr = htonl(INADDR_ANY); // 使用 INADDR_ANY 绑定到所有可用接口(通常用于服务器)
4. sin_zero:
- 类型:char[8]
- 说明:保留字段,用于填充 sockaddr_in 结构体,使其大小与 struct sockaddr 相同。通常应将其置为 0,不会使用其中的数据。
调用bind函数进行绑定:
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(8081);
//如果只想在本机上进行访问,bind函数地址可以使用本地回环地址
//如果只想被局域网的内部机器访问,那么bind函数地址可以使用局域网地址
//如果希望被公网访问,那么bind函数地址可以使用INADDR_ANY or 0.0.0.0
if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {
cout << "bind listen socket error" << endl;
return -1;
}
三、启动listen监听
listen()
函数用于将一个套接字(Socket)设置为监听状态,使其可以接受来自客户端的连接请求。它是 TCP 服务器中必不可少的步骤之一,在创建套接字和绑定地址之后调用。
int listen(int sockfd, int backlog);
1. sockfd:
- 类型:int
- 说明:指定用于监听的套接字文件描述符,这个套接字必须已经使用 socket() 创建并通过 bind() 绑定了地址和端口。
2. backlog:
-
类型:int
-
说明:指定等待连接队列的最大长度,即在 Socket 被 accept() 接受之前可以排队等待的最大客户端连接数量。
-
常用值:
- SOMAXCONN:一个系统定义的常量,表示系统允许的最大连接数。这是推荐的值,因为它可以自动调整到系统允许的最大值。 - 或者可以设置为一个具体的数字(如 5、10),但是 SOMAXCONN 更常见。
返回值:
成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误原因。
示例代码:
// 启动监听
if (listen(listenfd, SOMAXCONN) == -1) {
cout << "listen error" << endl;
return -1;
}
cout << "开始监听" << endl;
四、调用accept函数接受连接
当有客户端连接请求时,调用accept函数接受连接,产生一个新的socket
(与客户端通信的socket)。accept()
函数用于从监听套接字(通常是服务器端的)中提取一个客户端的连接请求,并为这个连接创建一个新的套接字。这是服务器接受客户端连接的关键步骤,调用 accept()
后,服务器可以与客户端进行数据交换。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
1. sockfd(监听套接字描述符):
- 类型:int
- 说明:这是服务器端用于监听的套接字描述符,必须通过 socket()、bind() 和 listen() 配置好并处于监听状态。通常是 listenfd。
2.addr(客户端地址结构体):
- 类型:struct sockaddr *
- 说明:用于存储客户端的地址信息,通常是 sockaddr_in 类型的指针,需要将其强制转换为 struct sockaddr * 类型。
- 服务器通过这个结构体获取客户端的 IP 地址和端口号。
3. addrlen(地址结构体长度):
- 类型:socklen_t *(通常是 int * 或 unsigned int *)
- 说明:一个指向整数的指针,指定 addr 结构体的大小,调用 accept() 后会被填充为实际的地址长度。
返回值:
- 成功时:返回一个新的套接字描述符 clientfd,专用于与该客户端的通信。
- 失败时:返回 -1,并设置 errno 以指示错误原因。
示例代码:
//创建一个临时的客户端socket
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
// 接受客户端连接
int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);
五、与客户端通信
(1)接受消息
recv()
函数用于从套接字中接收数据。它是一个阻塞函数,会一直等待数据的到来,直到接收到数据或发生错误。
int recv(int sockfd, void *buf, size_t len, int flags);
1. sockfd(套接字描述符):
- 类型:int
- 说明:用于接收数据的套接字描述符,这里通常是 accept() 返回的客户端套接字描述符 clientfd。
2. buf(接收缓冲区):
- 类型:void *
- 说明:指向存储接收到的数据的缓冲区,这里是 recvBuf。
3. len(缓冲区大小):
- 类型:size_t
- 说明:指定接收缓冲区的最大字节数,这里是 32,表示最多接收 32 个字节的数据。
4. flags(标志):
- 类型:int
- 说明:用于控制接收行为,常用值:
- 0:默认行为,阻塞接收数据。
- MSG_DONTWAIT:非阻塞接收。
- MSG_PEEK:查看数据但不从缓冲区移除数据。
返回值
- 成功时,返回实际接收到的字节数。
- 返回 0 表示对方关闭了连接。
- 返回 -1 表示发生错误,并设置 errno 以指示错误原因。
(2)发送消息
send()
函数用于向套接字发送数据,将数据从指定的缓冲区发送到对端。
int send(int sockfd, const void *buf, size_t len, int flags);
1. sockfd(套接字描述符):
-
类型:int
-
说明:用于发送数据的套接字描述符,这里是 clientfd。
** 2. buf(发送缓冲区):** -
类型:const void *
-
说明:指向包含要发送的数据的缓冲区,这里是 recvBuf。
3. len(发送数据的大小):
-
类型:size_t
-
说明:指定发送的数据字节数,这里是 strlen(recvBuf),表示发送 recvBuf 缓冲区中字符串的实际长度。
4. flags(标志): -
类型:int
-
说明:用于控制发送行为,常用值:
- 0:默认行为,阻塞发送数据。 - MSG_DONTWAIT:非阻塞发送。 - MSG_NOSIGNAL:避免发送时产生 SIGPIPE 信号。
返回值
- 成功时,返回实际发送的字节数。
- 返回 -1 表示发送失败,并设置 errno 以指示错误原因。
示例代码:
//将接受到消息返回给客户端
if (clientfd != -1) {
char recvBuf[32] = {0};
// 从客户端接受数据
int ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0) {
cout << "recv data from cilent , data:" << recvBuf << endl;
// 将接收到的数据原封不动地发给客户端
ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
if (ret != strlen(recvBuf)) {
cout << "send data error" << endl;
} else {
cout << "send data to client successfully, data " << recvBuf <<endl;
}
} else {
cout << "recv data error" <<endl;
}
close(clientfd);
}
六、调用close函数关闭socket
close()
函数用于关闭一个打开的文件描述符,包括套接字文件描述符。它是网络编程中管理资源的关键步骤,用于释放套接字所占用的系统资源,断开连接并停止进一步的通信。
int close(int fd);
1. fd(文件描述符):
- 类型:int
- 说明:指定需要关闭的文件描述符。在网络编程中,这个文件描述符通常是一个套接字,例如监听套接字 listenfd 或连接套接字 clientfd。
返回值
- 成功时:返回 0。
- 失败时:返回 -1,并设置 errno 以指示错误原因。
示例代码:
// 关闭监听socket
close(listenfd);
全部代码
#include <iostream>
#include <sys/types.h> //基本系统数据类型
#include <arpa/inet.h> //网络信息转换
#include <unistd.h> //POSIX系统API访问
#include <string.h>
using namespace std;
int main() {
// 创建一个监听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
//常见的AF_INET──指定为IPv4协议,AF_INET6──指定为IPv6,AF_LOCAL──指定为UNIX 协议域
//套接口可能的类型有:SOCK_STREAM字节流、SOCK_DGRAM数据报、SOCK_SEQPACKET有序分组、SOCK_RAW原始套接口
//传输协议TCP/UDP,这里默认0
if (listenfd == -1) {
cout << " create listen socket error " << endl;
return -1;
}
// 初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(8081);
//如果只想在本机上进行访问,bind函数地址可以使用本地回环地址
//如果只想被局域网的内部机器访问,那么bind函数地址可以使用局域网地址
//如果希望被公网访问,那么bind函数地址可以使用INADDR_ANY or 0.0.0.0
if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {
cout << "bind listen socket error" << endl;
return -1;
}
// 启动监听
if (listen(listenfd, SOMAXCONN) == -1) {
cout << "listen error" << endl;
return -1;
}
cout << "开始监听" << endl;
while (true) {
// 创建一个临时的客户端socket
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
// 接受客户端连接
int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);
if (clientfd != -1) {
char recvBuf[32] = {0};
// 从客户端接受数据
int ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0) {
cout << "recv data from cilent , data:" << recvBuf << endl;
// 将接收到的数据原封不动地发给客户端
ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
if (ret != strlen(recvBuf)) {
cout << "send data error" << endl;
} else {
cout << "send data to client successfully, data " << recvBuf <<endl;
}
} else {
cout << "recv data error" <<endl;
}
close(clientfd);
}
}
// 关闭监听socket
close(listenfd);
return 0;
}
注:
处理多个TCP连接的可以查看:使用epoll处理多个TCP线程连接