Linux下基于TCP协议的Socket套接字编程(客户端服务端)入门详解

news2024/11/17 21:46:13

写在前面:

本篇博客探讨&实践环境如下:
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_instruct 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 运行客户端程序

接下来我们重新打开一个终端,运行客户端程序
在这里插入图片描述
我们再看看服务端的情况:
在这里插入图片描述
当然这只是简单的客户端&服务端简单逻辑示例,作为入门介绍,后续还会继续补充。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/546500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能Python-python3加密解密

Python3加密解密——确保你的数据安全 在今天的数字化时代&#xff0c;数据已经成为最重要的财产之一。然而&#xff0c;随着互联网的流行&#xff0c;数据泄露和非法访问变得越来越普遍。因此&#xff0c;确保数据的安全性变得非常重要。加密解密是目前最受欢迎的数据安全措施…

chatgpt赋能Python-python3_8如何下载

Python 3.8如何下载 Python 3.8是一种高级编程语言&#xff0c;具有简单易于学习和高度可读性的特点。这篇文章旨在为您提供Python 3.8的下载方法。 什么是Python 3.8 Python 3.8是Python编程语言的最新版本&#xff0c;于2019年10月发布。它引入了许多新特性和功能&#xf…

分布式消息中间件RocketMQ的安装与启动

RocketMQ 安装与启动 基本概念 消息(Message)&#xff1a;消息是指&#xff0c;消息系统所传输信息的物理载体&#xff0c;生产和消费数据的最小单位&#xff0c;每条消息必须属于一个主题。主体(Topic)&#xff1a; Topic表示一类消息的集合&#xff0c;每个主题包含若干条消…

免接管余压探测器的优势是什么?

余压传感器&#xff08;探测器&#xff09;是什么&#xff1f; 余压传感器&#xff08;探测器&#xff09;主要用于测量两个位置之间的压差。 余压传感器&#xff08;探测器&#xff09;的作用&#xff1f; 流量测量&#xff1a;在管道中&#xff0c;通过测量两点之间的压差&…

头歌计算机组成原理实验—运算器设计(5) 第5关:32位快速加法器设计

第5关&#xff1a;32位快速加法器设计 实验目的 帮助学生理解成组进位产生函数&#xff0c;成组进位传递函数的概念&#xff0c;熟悉 Logisim 平台子电路的概念&#xff0c;能利用前述实验封装好的4位先行进位子电路以及4位快速加法器子电路构建16位、32位、64位快速加法器&a…

Day42【动态规划】背包问题详解、416.分割等和子集

0-1背包问题详解&#xff1a;二维数组 文章讲解 视频讲解 0-1 背包问题&#xff1a;有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight[i]&#xff0c;价值是 value[i]&#xff0c;每件物品只能用一次&#xff0c;求解将物品装入背包里物品价值总和最大…

深入理解 Go sync.Map

前言 Go 语言原生 map 并不是线程安全的&#xff0c;要对它进行并发读写操作时&#xff0c;一般有两种选择&#xff1a; 原生map搭配Mutex或RWMutex使用sync.Map 和原生map搭配Mutex或RWMutex相比&#xff0c;sync.Map在以下场景更有优势&#xff1a; 读多写少 修改&#x…

Golang 1.18 新特性模糊测试

一、Go 1.18新特性一览 0.官方博客&#xff1a;跳转 1.支持泛型 2.模糊测试 3.工作空间 4.性能提升 二、模糊测试介绍 模糊测试 &#xff08;fuzz testing, fuzzing&#xff09;是一种软件测试技术。其核心思想是將自动或半自动生成的随机数据输入到一个程序中&#xff0…

chatgpt赋能Python-python3怎么保存

Python 3如何高效地保存数据 Python是一门广泛使用的编程语言之一&#xff0c;其强大的工具和库使其成为从数据分析到机器学习等领域的首选语言。在处理大量数据时&#xff0c;数据的存储和访问变得非常关键。在这篇文章中&#xff0c;我们将讨论Python 3中如何保存数据以提高…

delphi6安装手册

DELPHI6安装手册 安装delphi6软件&#xff1a; 运行&#xff1a;\\dev2000 选择&#xff1a;set up→语言及开发工具→DELPHI6→双击图标install.exe 先后安装Delphi6和TeamSource 安装delphi6时会出现的要填序列号的情况 delphi6的安装序列号&#xff1a;在同一路径下双击ke…

chatgpt赋能Python-python3_9怎么调成黑色背景

Python是一种高级编程语言&#xff0c;它的版本不断发展和改进。最新发布的Python 3.9版本为用户提供了更多的新特性和改进&#xff0c;其中包括能够自定义代码编辑器背景颜色的新功能。本篇文章将介绍如何在Python 3.9中调整编辑器背景颜色为黑色&#xff0c;并探讨这个功能的…

三十九、分布式事务、seata、配置微服务客户端

1、事务 事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列SQL操作&#xff0c;这些操作作为一个整体一起向系统提交&#xff0c;要么都执行、要么都不执行。 1.1 ACID事务的特点 原子性: 一致性&#xff1a;隔离性持久性 1.2 事务并发带来的问题 脏读 幻读 不可重复读 …

TCL字符串操作

format命令 因为 TCL 把所有的输入都当作字符串看待&#xff0c;所以 TCL 提供了较强的字符串操作功能&#xff0c;TCL 中与 字符串操作有关的命令有&#xff1a;string、format、regexp、regsub、scan 等。 语法&#xff1a;format formatstring ?vlue value...? format …

【Redis】电商项目秒杀问题之下单接口优化:Redis缓存、MQ以及lua脚本优化高并发背景下的秒杀下单问题

目录 一、优化思路 二、缓存库存与订单 1、库存缓存的redis数据结构 2、订单信息缓存的redis数据结构 三、整体流程 四、lua脚本确保权限校验操作的原子性 一、优化思路 【Redis】电商项目秒杀问题之超卖问题与一人一单问题_1373i的博客-CSDN博客https://blog.csdn.net/q…

chatgpt赋能Python-python3_打印

Python3 打印&#xff1a;一篇介绍性SEO文章 如果你是一名Python编程工程师&#xff0c;那么你一定知道在Python中打印是一项基本技能。在Python3中&#xff0c;打印已经发生了一些变化&#xff0c;本文将介绍Python3中打印的新特性和使用方法&#xff0c;并为您提供一些最佳实…

算法设计与分析:随机化算法(作业-必做)(头歌实验)

第1关&#xff1a;硬币实验 任务描述 本关任务&#xff1a;计算机产生的伪随机数来模拟抛硬币试验。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;1.如何获取数组的长度&#xff0c;2.如何遍历数组。 随机数 随机数在随机化算法设计中扮演着十分重要的角…

云原生CAx软件:gRPC

gRPC是Google基于HTTP/2协议开发的一套开源、跨平台的高性能RPC框架&#xff0c;可用于连接微服务架构内的各种服务&#xff0c;亦可以连接客户端与后端服务。 Ref. from gRPC gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can…

策划能力提升攻略:让你成为行业大咖

策划能力的提高是没有立竿见影的&#xff0c;首先你了解策划的本质吗?了解市场营销的本质吗?了解战略和策略的关系吗? 不是经常把什么IMC/USP/4P/4C/DNA/核心价值挂在嘴边&#xff0c;会做做波特SWOT分析、用用BCG的模型、MINKSY的7S模型这些就是策划了的。 别人的理论你可…

WEB AK赛

文章目录 web1_观字SSRF常见的URL绕过方式 web2_观星web3_观图web4_观心签到_观己 web1_观字 <?php#flag in http://192.168.7.68/flag if(isset($_GET[url])){$url $_GET[url];$protocol substr($url, 0,7);if($protocol!http://){die(仅限http协议访问);}if(preg_matc…

安装Maven 3.6.1:图文详细教程(适用于Windows系统)

一、官网下载对应版本 推荐使用maven3.6.1版本&#xff0c;对应下载链接&#xff1a; Maven3.6.1下载地址 或者&#xff0c;这里提供csdn下载地址&#xff0c;点击下载即可&#xff1a; Maven3.6.1直链下载 其他版本下载地址&#xff1a; 进入网址&#xff1a;http://mave…