网络编程(socket 编程)

一.概念

网络编程是指通过计算机网络进行数据交换的过程。它涉及创建网络应用程序,使不同的计算机能够相互通信。网络编程的核心是使用套接字(Socket)进行数据传输,所以有时我们也直接叫socket编程

1.网络通信协议

在学习这个之前我们先学习一下几种网络模型

  • OSI模型:七层结构,每一层都有详细的功能定义,但实际应用中较少使用。
  • TCP/IP模型:四层结构,是互联网的基础,实际应用广泛。
  • 五层模型:结合了OSI模型和TCP/IP模型的优点,简化了OSI模型,同时包含了TCP/IP模型的核心内容,是教学和理解网络通信的常用模型。

最常用的还是4层模型(下一节会对其展开描述),而本文开头所讲的socket就是位于应用层和传输层之间的一个接口。它提供了一种编程接口,使得应用程序可以通过网络进行通信。

2. TCP/IP协议

TCP/IP协议(Transmission Control Protocol/Internet Protocol)是互联网通信的基础协议,它定义了数据如何在网络中进行传输和路由。TCP/IP协议栈包括四个主要层次,每个层次负责不同的功能。

2.1TCP/IP协议栈的四个层次

  1. 应用层(Application Layer)

    • 负责处理特定的应用程序细节。
    • 常见协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)等。
  2. 传输层(Transport Layer)

    • 负责端到端的可靠数据传输。
    • 主要协议:TCP(传输控制协议)和UDP(用户数据报协议)。
    • TCP提供可靠的、面向连接的服务,确保数据按顺序到达。
    • UDP提供不可靠的、无连接的服务,适用于实时应用。
  3. 网络层(Internet Layer)

    • 负责数据包的路由和转发。
    • 主要协议:IP(互联网协议)。
    • IP负责将数据包从源地址传输到目的地址。
  4. 链路层(Link Layer)

    • 负责在物理网络(如以太网、Wi-Fi)上传输数据。
    • 主要协议:以太网协议、Wi-Fi协议等。

2.2 TCP/IP数据传输过程

  1. 应用层:应用程序生成数据,并通过应用层协议(如HTTP)进行封装。
  2. 传输层:数据被分割成数据段,并添加TCP或UDP头部。
  3. 网络层:数据段被封装成数据包,并添加IP头部。
  4. 链路层:数据包被封装成帧,并添加链路层头部和尾部。
  5. 物理层:帧通过物理介质(如电缆、光纤)传输。

2.3 tcp的”三次握手“和”四次挥手“

TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

  1. 第一次握手:客户端向服务器发送一个SYN(同步)报文,请求建立连接。此时,客户端进入SYN_SENT状态。
  2. 第二次握手:服务器收到SYN报文后,返回一个SYN-ACK(同步-确认)报文,表示服务器同意建立连接。此时,服务器进入SYN_RECEIVED状态。
  3. 第三次握手:客户端收到SYN-ACK报文后,发送一个ACK(确认)报文给服务器,确认连接已建立。此时,客户端和服务器都进入ESTABLISHED状态,连接正式建立。

  1. 第一次挥手:客户端向服务器发送一个FIN(结束)报文,请求释放连接。此时,客户端进入FIN_WAIT_1状态。
  2. 第二次挥手:服务器收到FIN报文后,返回一个ACK报文,确认收到客户端的释放请求。此时,服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
  3. 第三次挥手:服务器处理完所有必要的数据后,向客户端发送一个FIN报文,请求释放连接。此时,服务器进入LAST_ACK状态。
  4. 第四次挥手:客户端收到FIN报文后,返回一个ACK报文,确认收到服务器的释放请求。此时,客户端进入TIME_WAIT状态,服务器收到ACK后进入CLOSED状态。经过一段时间后,客户端也进入CLOSED状态,连接正式释放

2.4 为什么不能是两次握手

在建立TCP连接的过程中,三次握手步骤不可或缺,其核心目的在于对双方的数据通信能力进行彻底的确认。

  • 第一次握手:客户端发起连接请求,发送一个包含SYN(同步序列编号)标志的数据包。这一步骤不仅检验了客户端的发送功能,也验证了服务端的接收能力。
  • 第二次握手:服务端响应,发送一个包含SYN和ACK(确认)标志的数据包,确认收到客户端的请求,并反馈自己的接收和发送状态。然而,此时服务端尚无法确认客户端的接收能力。
  • 第三次握手:客户端再次发送一个包含ACK标志的数据包,确认服务端的响应。这一动作使得服务端得以确认客户端的接收和发送功能均正常,同时自己也完成了对双方通信能力的全面验证。

因此,三次握手不仅是确认双方通信能力的关键,也是确保数据传输无误、连接稳定的重要保障。简而言之,仅通过两次握手无法达到同样的目的,因为无法完全验证双方的接收和发送能力。

2.5 为什么不能是三次挥手

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

2.6 UDP

提供无连接的、不可靠的数据报服务。适用于对速度要求高,但对数据完整性要求不高的场景,如视频流、语音通信等。

二.socket编程

在前面的时候就已经多次提到了socket编程,Socket编程是网络编程中的一个重要概念,它允许不同主机上的进程进行通信,可以把socket想象成一个管道,通过它可以实现数据的传输,所以这个一般是成对的出现的,从一个socket输入,然后从另外一个socket接收信息。

1.socket开发流程(服务器)

1. 创建Socket

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • AF_INET:指定使用IPv4地址。
  • SOCK_STREAM:指定使用TCP协议。
  • 0:通常用于指定协议,对于TCP,这个值通常是0。

2. 准备服务器地址结构

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 允许绑定到任何IP地址
servaddr.sin_port = htons(8080); // 端口号
  • sockaddr_in:IPv4地址结构。
  • memset:清零地址结构。
  • htonlhtons:主机到网络字节顺序转换。

3. 绑定Socket到地址

bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  • bind:将Socket绑定到指定的地址和端口。

4. 监听连接

listen(sockfd, 10); // 第二个参数是队列长度
  • listen:使Socket变为监听状态,允许指定等待连接的最大数量。

5. 接受连接

struct sockaddr_in cliaddr;
int clilen = sizeof(cliaddr);
int newsockfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
  • accept:等待并接受客户端的连接请求。

6. 读写数据

char buffer[1024];
int n = read(newsockfd, buffer, sizeof(buffer));
if (n > 0) {
    write(newsockfd, buffer, n);
}
  • read:从Socket读取数据。
  • write:向Socket写入数据。

7.关闭连接

close(newsockfd);
close(sockfd);

  • close:关闭Socket。

2.socket开发流程(客户端)

1. 创建Socket

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

  • AF_INET:指定使用IPv4地址。
  • SOCK_STREAM:指定使用TCP协议。
  • 0:通常用于指定协议,对于TCP,这个值通常是0。

2.连接指定计算机的端口

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • sockfd:这是已经创建好的套接字描述符。
  • addr:这是一个指向sockaddr结构的指针,该结构包含了要连接的服务器的地址信息。
  • addrlen:这是一个表示addr指向的结构大小的socklen_t类型变量。

3.向socket中读写信息

char buffer[1024];
int n = read(newsockfd, buffer, sizeof(buffer));
if (n > 0) {
    write(newsockfd, buffer, n);
}

  • read:从Socket读取数据。
  • write:向Socket写入数据。

4.关闭套接字

close(sockfd);

  • close:关闭Socket。

三.TCP 客户端服务端通信

服务器

// server

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

int main(int argc, char const *argv[])
{
    // 创建套接字
    // int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 服务地址
    // struct sockaddr_un addr;
    // addr.sun_family = AF_UNIX;
    // strcpy(addr.sun_path, "my_server2");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);               // 端口号
    // 0.0.0.0
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  // IP地址

    // inet_addr 函数默认执行了 主机字符序列到网络字符序列的转换
    // addr2.sin_addr.s_addr = inet_addr("127.0.0.1");
    // addr2.sin_addr.s_addr = htonl(INADDR_ANY);
    // addr2.sin_port = htons(9000);

    // 绑定地址
    bind(server_fd, (struct sockaddr *) &addr, sizeof(addr));

    // 监听
    listen(server_fd, 9);
    printf("服务器启动\n");

    // struct sockaddr_un addr_client;
    // addr_client.sun_family = AF_UNIX;
    // int addr_len = sizeof(addr_client);

    struct sockaddr_in c_addr;
    int c_addr_len = sizeof(c_addr);
    
    // 子进程结束
    // 忽略
    signal(SIGCHLD, SIG_IGN);

    while (1)
    {
        // 接收请求,建立了 TCP 连接,获得了一个新的客户端套接字
        printf("等待客户端请求...\n");
        int client_fd = accept(
            server_fd, 
            (struct sockaddr *)&c_addr, 
            &c_addr_len);
        
        // 并发:进程、线程
        // 异步
        if (fork() == 0)
        {
            int pid = getpid();

            // 读写数据
            char buf[32];
            for (size_t i = 0; i < 9; i++)
            {
                read(client_fd, &buf, 32);
                printf("%d 收到 %ld:%s\n", pid,i, buf);

                // TODO
                char msg[32] ;
                fgets(msg, 32, stdin);
                
                write(client_fd, &msg, 32);
            }
            //  退出
            exit(EXIT_SUCCESS);
        }
        else
        {
            close(client_fd);
        }
    }

    return 0;
}

客户端

// client

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char const *argv[])
{

    // 创建套接字
    // 1. 域
    // 2. 套接字类型
    // 3. 协议
    // int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 服务器地址
    // struct sockaddr_un addr;
    // addr.sun_family = AF_UNIX;
    // strcpy(addr.sun_path, "my_server2");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);
    // 服务器
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    // addr.sin_port = 9000;

    // 连接服务器
    // 1. 套接字
    // 2. 目标地址
    // 3. 目标地址长度
    int r = connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr));
    if (r == -1)
    {
        printf("连接失败\n");
        return EXIT_FAILURE;
    }

    char ch[32];
    for (size_t i = 0; i < 9; i++)
    {
        fgets(ch, 32, stdin);
        ch[strcspn(ch, "\n")] = '\0';
        // 读写数据
        write(socket_fd, &ch, sizeof(ch)-1);
        printf("已发送数据 %ld\n", i);

        read(socket_fd, &ch, sizeof(ch));
        printf("接收到:%s\n", ch);
    }

    // 关闭套接字
    close(socket_fd);
    return 0;
}

这是一段简单的服务器和客户端代码,演示了基于TCP的网络通信,客户端从标准输入读取数据,发送到服务器,并等待服务器的响应,然后打印出接收到的数据。

好的到这里本期的内容就结束了,如有错误,欢迎大家批评指正,感谢您的观看

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值