写在前面:
本篇博客探讨&实践环境如下:
1.操作系统: Linux
2.版本(可以通过命令 cat /etc/os-release 查看版本信息):PRETTY_NAME=“CentOS Linux 7 (Core)”
编程语言:C
一、socket 是什么?
常常说socket 、套接字 那么socket 到底指的是什么?
socket 本质上是一个抽象的概念,它是一组用于网络通信的 API,提供了一种统一的接口,使得应用程序可以通过网络进行通信。在不同的操作系统中,socket 的实现方式可能不同,但它们都遵循相同的规范和协议,可以实现跨平台的网络通信。
socket 的实现通信的原理是基于网络协议栈。
当应用程序创建一个 socket 并指定协议族、类型和使用的协议后,操作系统会创建一个对应的套接字,并把它加入到协议栈中。
协议栈是一个由多个层次协议组成的网络协议体系结构,它负责对数据进行封装和解封装,并确保数据能够在网络上正确传输。
当应用程序通过 socket 发送数据时,操作系统会将数据传递给协议栈的上层协议,该协议会对数据进行封装并添加一些必要的信息,例如目标 IP 地址和端口号等。然后将封装后的数据传递给下一层协议,直到数据最终被封装成一个网络包并通过网络发送到目标主机。
当目标主机收到网络包后,协议栈会对数据进行解封装,并将数据传递给操作系统中的套接字。如果该套接字是一个监听套接字,操作系统会创建一个新的套接字来处理连接请求,并将新的套接字加入到协议栈中。如果该套接字是一个已连接套接字,操作系统会将数据传递给应用程序处理。
总之,socket 实现通信的原理是基于网络协议栈,通过将数据封装成网络包并通过网络传输,实现了应用程序之间的通信。操作系统负责管理套接字和协议栈,确保数据能够正确传输。
我们先了解一下一个客户端使用socket 套接字与服务端进行通信流程:
1.1 客户端&服务器端socket通信流程图:
服务端流程:
接下来做具体的函数讲解(不枯燥,每一步都有示例和运行演示)
1.2 socket 系统接口函数
socket 是一个系统接口函数,由操作系统提供,用于实现网络编程的功能。通过 socket 函数,应用程序可以创建套接字、绑定地址、监听连接、发送和接收数据等操作,从而实现网络通信。
1.2.1 创建套接字 socket
函数:
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
参数详解:
1.domain 参数
domain 参数指定套接字的协议族,常见的协议族有 AF_INET(IPv4)、AF_INET6(IPv6)和 AF_UNIX(本地套接字)等。
2.type 参数
type 参数指定套接字的类型,常见的类型有 SOCK_STREAM(流套接字/TCP 套接字)和 SOCK_DGRAM(数据报套接字/UDP套接字)等。
3.protocol 参数
protocol 参数指定使用的协议,常见的协议有 IPPROTO_TCP(TCP 协议)和 IPPROTO_UDP(UDP 协议)等。如果指定为 0,则会根据 domain 和 type 参数自动选择协议。
返回值:
socket 函数的返回值为新创建的套接字的文件描述符,如果创建失败则返回 -1。
代码示例:
#include<stdio.h>
#include<sys/socket.h>
int main()
{
int sockid = 0;
sockid =socket(AF_INET,SOCK_STREAM,0);
if(sockid < 0)
{
printf("socket creat fail\n");
}
else
{
printf("socket crear sucess and socket id is %d\n", sockid);
}
return 0;
}
运行结果:
返回值是一个文件描述符,也就是套接字的一个操作句柄,通过这个操作句柄来设置协议、端口、地址等等,这样才能实现网络中两个主机之间进行通讯。
1.2.2 设置地址、端口号等
既然要和另一个主机进行网络通信,那么肯定需要知道该主机的地址,就好比我要给你写信也得知道你家的地址才能把信送到,因此需要设置IP地址信息、端口号,而端口号又是什么呢?
1.2.2.1 端口号:
端口号是在计算机网络中用于标识应用程序或服务的数字标识符。在 TCP/IP 协议中,端口号是一个 16 位的整数,取值范围是 0~65535。
每个端口号都与一个唯一的应用程序或服务相关联,用于标识应用程序或服务在网络中的位置,实现网络通信。
IP地址用于确定,数据发送到哪一个主机上面,而这个主机同时在运行很多程序,为了确保数据能够正确交付给对应的程序,那么就用端口号来进行标识,一个端口号就可以确定唯一的一个进程,并且一个进程也可以拥有多个端口号,但是一个端口号只能和一个进程绑定
就好比上面的送信件,确定你家地址之后就可以送到你的家门口,而想再确定一点,送到你的房间门口,确保不会送到你爸爸妈妈门口的话,就需要在填上你的房间号
上述都是为了确保数据能够正确发送给对方,因此在创建socket 套接字之后还需要,对上述的信息设置,然后和套接字进行绑定(客户端不需要绑定,服务端的绑定操作,会在后面说到)
1.2.2.2 地址结构体
端口号、IPV4/IPV6、IP地址这些数据组合起来成为一个地址结构体,方便地址信息的设置与绑定
1.2.2.2.1 IPV4 地址结构体
结构体名称:
#include<netinet/in.h>
struct sockaddr_in
struct sockaddr_in 是一个 IPv4 地址结构体,用于在网络编程中表示 IPv4 地址和端口号。
结构体定义:
struct sockaddr_in {
sa_family_t sin_family; /* 套接字协议族 IPV4 AF_INET */
in_port_t sin_port; /* 端口号 例如 8888*/
struct in_addr sin_addr; /* IP 地址 */
};
struct in_addr {
uint32_t s_addr; /* IP地址 */
};
1.2.2.2.2 IPV6 地址结构体
结构体名称:
#include<netinet/in.h>
struct sockaddr_in6
struct sockaddr_in6 是一个IPv6 地址结构体是一个用于在网络编程中表示 IPv6 地址的结构体
结构体定义如下:
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 */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 地址 */
};
示例,设置IPV4 端口号、协议族、IP地址:
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8080);
1.2.2.2.3 “127.0.0.1” 是什么
在 Socket 编程中,127.0.0.1 是一个特殊的 IP 地址,表示本地主机(localhost)。当客户端和服务器在同一台计算机上运行时,可以使用 127.0.0.1 作为服务器地址,以便客户端连接到服务器。
使用 127.0.0.1 作为服务器地址的优点是:
1.可以方便地在同一台计算机上测试客户端和服务器的通信,无需使用其他计算机或网络。
2.127.0.0.1 是一个固定的地址,不会因为网络环境的变化而改变,因此可以避免因 IP 地址变化而导致的连接问题。
在实际生产环境中,如果客户端和服务器不在同一台计算机上,则应该使用实际的 IP 地址或域名来连接服务器。在这种情况下,需要确保客户端和服务器之间的网络通信正常,并且需要考虑网络安全和防火墙等问题。
上面示例是作为一个客户端的设置,因为客户端需要访问服务端地址因此需要设置服务端地址,接下来作一段客户端socket 通信代码示例:
二、 socket 通信代码示例
2.1 客户端简单代码示例:
客户端socket 之TCP 通信流程图
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT 8888 //端口号
int main(int argc, char const *argv[]) {
int sock = 0, valread;
struct sockaddr_in serv_addr; //IPV4 地址结构体
char *hello = "Hello from client"; //发送数据
char buffer[1024] = {0};
// 创建 TCP 套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
// 设置服务器地址和端口
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将 IPv4 地址从点分十进制转换为二进制
if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
// 发送数据
send(sock, hello, strlen(hello), 0);
printf("Data sent: %s\n", hello);
// 接收服务器响应数据
valread = read(sock, buffer, sizeof(buffer));
printf("Response received: %s\n", buffer);
// 关闭连接
close(sock);
return 0;
}
2.1.1 htons / inet_pton /connect 函数说明
2.1.1.1 htons 网络字节序转换函数
htons 是一个网络字节序转换函数,用于将 16 位整数从主机字节序(即本地字节序)转换为网络字节序(即大端字节序)。
这里涉及到一个知识点:大端和小端
大端(Big-Endian)和小端(Little-Endian)是两种不同的字节序排列方式。在计算机中,数据类型的存储是以字节为单位的,不同的计算机体系结构采用不同的字节序方式。
大端字节序
是指将高位字节存放在低地址,低位字节存放在高地址,即数据的高字节存放在内存的低地址处,低字节存放在高地址处。例如,十六进制数 0x12345678 在大端字节序中的存储方式为 12 34 56 78。
小端字节序
则是将低位字节存放在低地址,高位字节存放在高地址,即数据的低字节存放在内存的低地址处,高字节存放在高地址处。例如,十六进制数 0x12345678 在小端字节序中的存储方式为 78 56 34 12。
那么如何判断自己是大端还是小端呢?
我们可以打开VS内存窗口查看变量
可以看到我本地的vs 编译器环境下是 小端机
同样可以通过下面的一段代码来判断大小端:
同时我们可以在 linux 环境下再来看看大小端情况:
2.1.1.2 inet_pton IP地址转换
inet_pton 是一个函数,用于将点分 十进制的 IP 地址(例如 “127.0.0.1” 转换为网络字节序的二进制数值,并存储到指定的内存空间中。inet_pton 函数定义在头文件 <arpa/inet.h> 中。
下面是 inet_pton 函数的函数原型:
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数详解:
参数 af 表示地址族,可以取值为 AF_INET 或 AF_INET6,分别表示 IPv4 和 IPv6 地址。
参数 src 是字符串类型的 IP 地址,即点分十进制格式的字符串。
参数 dst 是指向用于存储转换结果的内存地址。
返回值:
inet_pton 函数返回值为整数类型,如果转换成功,返回值为 1;
如果转换失败,返回值为 0;
如果地址族 af 不支持,返回值为 -1。
注意:
inet_pton 函数会将转换结果存储到 dst 指针所指向的内存空间中。
对于 IPv4 地址,需要分配 4 个字节的内存空间来存储转换结果;
对于 IPv6 地址,需要分配 16 个字节的内存空间来存储转换结果。
代码示例:
#include <stdio.h>
#include <arpa/inet.h>
int main() {
const char *ip = "127.0.0.1";
struct in_addr addr;
if (inet_pton(AF_INET, ip, &addr) <= 0) {
printf("Failed to convert IP address\n");
return -1;
}
printf("IP address: 0x%x\n", addr.s_addr);
return 0;
}
运行结果:
2.1.1.3 connect 连接服务器
connect 函数是在客户端中用于连接服务器的函数,它定义在头文件 <sys/socket.h> 中。
下面是 connect 函数的函数原型:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
1.参数 sockfd 表示要连接的套接字,即由 socket 函数返回的文件描述符。
2.参数 addr 是指向目标服务器地址的指针,通常是一个 sockaddr_in(IPV4 结构体) 或 sockaddr_in6(IPV6 结构体) 结构体类型的指针。
3.参数 addrlen 表示目标服务器地址的长度。
返回值:
connect 函数返回值为整数类型,
如果连接成功,返回值为 0;
如果连接失败,返回值为 -1。
注意:
在使用 connect 函数之前,需要先创建一个套接字,并将其绑定到本地地址(一般服务端需要)。通常,在客户端中,不需要将套接字绑定到本地地址。
在调用 connect 函数之前,需要将目标服务器地址与端口号设置到 sockaddr_in 或 sockaddr_in6 结构体中,并将其地址传递给 connect 函数。
在调用 connect 函数之后,客户端需要使用 send 和 recv 函数来发送和接收数据。
示例:
// 发送数据
send(sock, hello, strlen(hello), 0);
printf("Data sent: %s\n", hello);
// 接收服务器响应数据
valread = recv(sock, buffer, sizeof(buffer));
printf("Response received: %s\n", buffer);
2.2 服务端socket通信示例
TCP socket 服务端 通信流程图:
服务端是用来接收客户端得请求的,熟悉tcp 三次握手的都知道,在没有新的客户端请求建立连接前处于一个监听状态,等到有新的客户端建立连接了,再开始建立连接、创建新的套接字、接收数据、发送数据等。
和客户端一样,首先也是创建套接字
2.2.1 创建socket 套接字,设置IP地址结构体
struct sockaddr_in server_address;//IPV4 地址结构体
int server_socket = 0;
// 创建一个TCP socket
if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
//设置IPV4地址结构体
// 指定服务器地址和端口
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY;
server_address.sin_port = htons(8000);
这里可以看到我们没有给服务器端设置确定的IP地址,而是给了一个 INADDR_ANY 的参数,这个参数代表什么呢?
INADDR_ANY 是一个特殊的IP地址,在IPv4中表示监听所有可用的网络接口。当使用INADDR_ANY时,socket将绑定到所有可用的网络接口上,这样就可以接收来自任何网络接口的数据包。
在实际使用中,通常将服务器socket绑定到INADDR_ANY地址,以便能够接受来自任何网络接口的客户端连接。例如,在使用TCP/IP协议进行通信的服务器程序中,服务器socket通常会使用INADDR_ANY地址来绑定服务器的IP地址和端口,以便能够接受来自任何客户端的连接请求。
这里我们要明确,这里的地址结构体里的IP地址一般是设置的对端地址信息,需要通讯的另一端地址,服务器和客户端是 多对一的一个逻辑关系,当然这里说的是只有一个服务器情况下,是多对一,多个服务器和多个客户端肯定是多对多的关系
因此服务器端需要先给一个默认值,然后处于一个监听状态,去监听有那些客户端给服务器端发送请求建立连接的新连接,通过获取客户端请求的信息,知道客户端的IP地址信息,端口号,等等,再建立一个新的套接字,然后用这个新的套接字和客户端进行通讯,收发数据等。
2.2.2 绑定服务器地址和端口
客户端一般不绑定地址和端口,客户端直接 connect ,connect 函数的参数里有 套接字的文件描述符、地址结构体信息、地址长度,可以直接通过这些信息和服务端进行连接,服务端绑定是为了便于客户端找到服务端,且服务端也会创建新的套接字和客户端进行连接。
2.2.2.1 bind 函数
函数:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:表示要绑定的 socket 描述符。
addr:表示要绑定的地址信息,是一个指向 struct sockaddr 类型的指针,可以是 struct sockaddr_in 或 struct sockaddr_in6 等结构体类型的指针。
addrlen:表示地址信息的长度,通常使用 sizeof 运算符获取。
返回值:
bind 函数成功执行时返回 0,失败返回 -1,并设置全局变量 errno 来指示错误类型。
注意:
bind 函数的作用是将一个 socket 绑定到一个具体的地址和端口上。这个地址和端口可以是任意的本地或远程地址和端口,但必须是未被其他 socket 使用的地址和端口。在 socket 编程中,服务器通常使用 bind 函数将一个 socket 绑定到一个特定的本地地址和端口上,以便客户端能够通过这个地址和端口找到服务器并连接到它。
这里之前讲过一个端口号只能被一个进程使用原则
代码示例:
// 绑定服务器地址和端口
if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
socket 创建以及地址信息、端口设置且绑定好了之后,接下来便开始斤进行监听,等待客户端进行连接
2.2.3 listen 监听
listen 函数是在 socket 编程中用于将一个 socket 设置为监听状态的系统函数。
函数:
int listen(int sockfd, int backlog);
参数:
sockfd 即就是 socket 套接字文件描述符
backlog:表示操作系统允许在该 socket 上排队等待的最大连接数。
返回值:
listen 函数成功执行时返回 0,失败返回 -1,并设置全局变量 errno 来指示错误类型
注意:
调用 listen 函数并不会阻塞程序执行,因为它只是将一个 socket 设置为监听状态,并没有开始接受连接请求。
要接受连接请求,服务器必须使用 accept 函数从等待队列中取出连接请求,并创建一个新的 socket 连接。
2.2.4 accept 接收连接
在 Socket 编程中,accept() 是服务器端常用的一个系统调用,用于接受客户端的连接请求并创建一个新的套接字来处理与该客户端的通信。
因为不断会有客户端发起请求,请求与服务端连接,并需要对这里客户端的请求进行处理
accpet() 函数示例:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
sockfd:表示监听套接字的文件描述符。
addr:表示传出参数,指向客户端地址的结构体指针。
addrlen:表示传入传出参数,传入的是指向客户端地址结构体的长度,传出的是客户端地址结构体的实际长度。
返回值:
如果 accept() 函数调用成功,则返回一个新的套接字描述符,这个套接字描述符用于和客户端进行通信。
如果调用失败,则返回 -1。
原理:
当一个客户端发起连接请求时,服务器端的 accept() 函数会从连接请求队列中取出一个连接请求,然后创建一个新的套接字用于和该客户端进行通信,并将该套接字的文件描述符返回给服务器端,服务器端可以使用该套接字描述符与客户端进行数据传输。
注意两个传出的参数:addr、addrlen 这两个参数可以得知客户端的地址结构体信息,这样服务端就知道了客户端的地址信息,便能够和客户端进行通讯
且 服务端需要不断循环地取出新的客户端请求,因此一般将 accept 放入到一个 while 循环中,循环的获取新的客户端连接请求。
2.2.5 接收客户端数据/向客户端发送数据
2.2.5.1 recv() 接收数据
recv() 函数是用于接收数据的函数,它的作用是从已连接的套接字中接收数据,并将数据存储到指定的缓冲区中。
函数:
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
函数参数:
sockfd 表示已连接的套接字描述符
buf 表示用于存储接收数据的缓冲区
len 表示缓冲区的长度
flags 表示接收数据的可选参数,通常设置为 0。
返回值:
recv() 函数的返回值表示实际接收到的数据的字节数
如果返回值为 0,则表示对端已经关闭连接;
如果返回值为 -1,则表示接收数据出现了错误,需要根据 errno 变量来确定具体的错误原因。
代码示例:
char buf[1024];
ssize_t n = recv(sockfd, buf, sizeof(buf), 0);
if (n == -1) {
// 接收数据出错,需要根据 errno 变量来确定具体的错误原因
perror("recv error");
} else if (n == 0) {
// 对端已经关闭连接
printf("peer closed the connection\n");
} else {
// 成功接收到 n 字节的数据
// 处理接收到的数据
// ...
}
2.2.5.2 send() 向客户端发送数据
函数作用:
send() 函数是用于发送数据的函数,它的作用是将指定的数据发送到已连接的套接字中
函数:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
函数参数:
sockfd 表示已连接的套接字描述符;
buf 表示待发送数据的缓冲区;
len 表示待发送数据的长度;
flags 表示发送数据的可选参数,通常设置为 0。
函数返回值:
send() 函数的返回值表示实际发送的数据的字节数
如果返回值为 -1,则表示发送数据出现了错误 需要根据 errno 变量来确定具体的错误原因。
代码示例:
char *msg = "Hello, world!";
int len = strlen(msg);
int bytes_sent = send(sockfd, msg, len, 0);
if (bytes_sent == -1) {
perror("send");
} else {
printf("Sent %d bytes\n", bytes_sent);
}
2.2.6 服务器端代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define MAX_CONNECTIONS 5
#define BUFFER_SIZE 1024
int main() {
int sockfd, client_sockfd, bytes_received;
struct sockaddr_in serv_addr, client_addr;
socklen_t client_addrlen = sizeof(client_addr);
char buffer[BUFFER_SIZE];
char *response = "Hello, client!";
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 配置服务器端地址、端口信息
bzero(&serv_addr, sizeof(serv_addr));//清空内容
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
// 绑定地址结构体到套接字上
if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 设置套接字为监听状态
if (listen(sockfd, MAX_CONNECTIONS) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// 接收新的客户端请求
client_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
if (client_sockfd == -1) {
perror("accept");
continue;
}
// 接收客户端数据
bytes_received = recv(client_sockfd, buffer, BUFFER_SIZE, 0);
if (bytes_received == -1) {
perror("recv");
close(client_sockfd);
continue;
}
printf("Received %d bytes from client: %s\n", bytes_received, buffer);
// 向客户端发送数据
if (send(client_sockfd, response, strlen(response), 0) == -1) {
perror("send");
close(client_sockfd);
continue;
}
printf("Sent response to client: %s\n", response);
// 关闭当前套接字
close(client_sockfd);
printf("Client connection closed\n");
}
// 关闭服务端套接字
close(sockfd);
return 0;
}
三、完整代码与示例
3.1客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd, bytes_sent, bytes_received;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE];
char *request = "Hello, server!";
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 配置地址结构体信息、端口号、IP地址等
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 向服务端发送请求连接
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("connect");
exit(EXIT_FAILURE);
}
printf("Connected to server: %s:%d\n", inet_ntoa(serv_addr.sin_addr), ntohs(serv_addr.sin_port));
// 向服务端发送请求、数据
bytes_sent = send(sockfd, request, strlen(request), 0);
if (bytes_sent == -1) {
perror("send");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Sent %d bytes to server: %s\n", bytes_sent, request);
// 接收服务端数据
bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);
if (bytes_received == -1) {
perror("recv");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Received %d bytes from server: %s\n", bytes_received, buffer);
// Close the socket
close(sockfd);
return 0;
}
3.2 服务器端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define MAX_CONNECTIONS 5
#define BUFFER_SIZE 1024
int main() {
int sockfd, client_sockfd, bytes_received;
struct sockaddr_in serv_addr, client_addr;
socklen_t client_addrlen = sizeof(client_addr);
char buffer[BUFFER_SIZE];
char *response = "Hello, client!";
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 配置服务器端地址、端口信息
bzero(&serv_addr, sizeof(serv_addr));//清空内容
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
// 绑定地址结构体到套接字上
if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 设置套接字为监听状态
if (listen(sockfd, MAX_CONNECTIONS) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// 接收新的客户端请求
client_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
if (client_sockfd == -1) {
perror("accept");
continue;
}
// 接收客户端数据
bytes_received = recv(client_sockfd, buffer, BUFFER_SIZE, 0);
if (bytes_received == -1) {
perror("recv");
close(client_sockfd);
continue;
}
printf("Received %d bytes from client: %s\n", bytes_received, buffer);
// 向客户端发送数据
if (send(client_sockfd, response, strlen(response), 0) == -1) {
perror("send");
close(client_sockfd);
continue;
}
printf("Sent response to client: %s\n", response);
// 关闭当前套接字
close(client_sockfd);
printf("Client connection closed\n");
}
// 关闭服务端套接字
close(sockfd);
return 0;
}
3.3 运行结果:
3.3.1 首先运行服务端程序
如果先运行客户端程序会出现下面的问题:
服务端还没有启动,那么客户端自然是无法连接服务端的
运行服务端程序:
此时服务端运行,处于监听状态,等待客户端发起连接请求
3.3.2 运行客户端程序
接下来我们重新打开一个终端,运行客户端程序
我们再看看服务端的情况:
当然这只是简单的客户端&服务端简单逻辑示例,作为入门介绍,后续还会继续补充。