socket编程|TCP

一.套接字概念

套接字(Socket)是一种用于网络通信的编程接口,它提供了一种机制,使得不同计算机上的应用程序能够通过网络进行通信和交换数据。

套接字可以看作是应用程序和网络之间的端点,它定义了应用程序与网络之间的通信规则和数据格式。通过套接字,应用程序可以创建、连接、发送和接收数据,实现与其他计算机上的应用程序进行通信。

套接字通常基于TCP/IP协议栈进行通信,其中常用的套接字类型有两种:

  1. 流式套接字(Stream Socket):使用基于流的TCP协议,提供可靠的面向连接的通信。流式套接字通过建立连接,提供可靠的、无差错的数据传输,保证数据的有序性和完整性。

  2. 数据报套接字(Datagram Socket):使用基于数据报的UDP协议,提供无连接的通信。数据报套接字以短的数据包(数据报)的形式传输数据,适用于实时性要求较高但不需要可靠性的情况。

使用套接字进行通信通常需要进行以下步骤:

  1. 创建套接字:应用程序通过调用系统API来创建一个套接字。

  2. 绑定套接字:将套接字与本地网络地址绑定,以便其他应用程序可以通过网络找到它。

  3. 监听连接请求(对于服务器):在服务器端,套接字会监听传入的连接请求。

  4. 建立连接(对于客户端):在客户端,套接字会发起连接请求与服务器建立连接。

  5. 发送和接收数据:已建立连接的套接字可以通过发送和接收数据来进行通信。

  6. 关闭套接字:通信完成后,应用程序可以关闭套接字来释放资源。

套接字编程提供了灵活和强大的网络通信功能,广泛应用于互联网、网络通信和分布式计算等领域。

网络通信过程中,套接字一定是成对出现的,一个套接字描述符有两个缓冲区,分别是写缓冲区,读缓冲区

二.预备知识

2.1网络字节序

网络字节序(Network Byte Order)是一种规定的数据表示方式,在网络通信中用于保证不同计算机之间的数据交换的正确性和一致性。

在计算机内部,数据一般以主机字节序(Host Byte Order)存储和处理。主机字节序可以是大端字节序(Big-Endian)或小端字节序(Little-Endian),具体取决于计算机体系结构。大端字节序表示高位字节在前、低位字节在后;小端字节序表示低位字节在前、高位字节在后。

为了在不同的计算机之间进行数据交换,需要使用统一的网络字节序。网络字节序采用的是大端字节序,因此在网络通信中,数据会按照从高位到低位的顺序进行传输。

为了方便在不同字节序之间进行转换,可以使用以下函数进行字节序转换:

  • htons():将16位(2字节)主机字节序转换为网络字节序。
  • ntohs():将16位(2字节)网络字节序转换为主机字节序。
  • htonl():将32位(4字节)主机字节序转换为网络字节序。
  • ntohl():将32位(4字节)网络字节序转换为主机字节序。

使用这些函数进行字节序的转换可以确保在不同计算机之间进行数据交换时,数据的字节顺序是一致的。

网络字节序-->大端法-->高位存低地址

主机字节序-->小端法-->高位存高地址

大小端区别

例如十六进制数值 0x12345678,我们可以将其分解成四个字节进行表示。

在大端存储中,高位字节存储在低地址处,低位字节存储在高地址处。因此,0x12 是高位字节,0x34 是次高位字节,0x56

是次低位字节,0x78 是低位字节。所以在大端存储中,表示为:

高地址 -> 低地址:0x12 0x34 0x56 0x78
在小端存储中,低位字节存储在低地址处,高位字节存储在高地址处。因此,0x78 是低位字节,0x56 是次低位字节,0x34

是次高位字节,0x12 是高位字节。所以在小端存储中,表示为:

高地址 -> 低地址:0x78 0x56 0x34 0x12

大小端判断

使用 intchar 类型的联合体可以进行字节序的判断,而不使用数组。

以下是一个示例代码:

c++ 复制代码
#include <stdio.h>

union EndianTest {
    int value;
    char byte;
};

int isLittleEndian() {
    union EndianTest test;
    test.value = 1;

    // 判断低字节的值是否为1
    return (test.byte == 1);
}

int main() {
    if (isLittleEndian()) {
        printf("小端字节序 (Little-Endian)\n");
    } else {
        printf("大端字节序 (Big-Endian)\n");
    }

    return 0;
}

在这个示例中,我们定义了一个联合体 EndianTest,它包含一个 int 类型的成员 value 和一个 char 类型的数组成员 byte

函数 isLittleEndian() 中,我们将数值 1 赋值给 value,然后通过检查 byte[0] 的值是否为 1 来判断字节序。如果 byte[0] 的值为 1,则表示是小端字节序;如果不为 1,则表示是大端字节序。

通过这个方法,我们可以使用 intchar 类型的联合体来判断当前计算机的字节序是大端还是小端。

2.2 IP地址转换函数

主机字节序转换为网络字节序

不常用方法

常用方法

头文件: #include<sys/socket.h>

#include<netinet/in.h>

#include<arpa/inet.h>

声明:

in_addr_t inet_addr(const char *strptr); //该参数是字符串

复制代码
      typedef uint32_t in_addr_t;  (通过vi -t 追一下)
        struct in_addr { 
                    in_addr_t   s_addr;
        }; 
 
 
功能:  主机字节序转为网络字节序
参数:  const char *strptr: 字符串
返回值: 返回一个无符号长整型数(无符号32位整数用十六进制表示), 
      否则NULL

网络字节序转换为主机字节序

不常用

常用

复制代码
头文件 :    #include <arpa/inet.h>  
           #include<sys/socket.h>  
           #include<netinet/in.h>  
声明:   char *inet_ntoa(stuct in_addr inaddr);
功能:   将网络字节序二进制地址转换成主机字节序。 
参数:  stuct in_addr in addr  : 只需传入一个结构体变量
返回值:  返回一个字符指针, 否则NULL;

2.3 sockaddr数据结构

复制代码
    头文件:
           #include<sys/types.h>  
           #include<sys/socket.h> 
           #include<netinet/in.h>  
           #include<netinet/ip.h>

   ipv4通信结构体:
            struct sockaddr_in {
                sa_family_t    sin_family;   ----协议族
                in_port_t      sin_port;     ----端口
                struct in_addr sin_addr;     ----ip结构体
            };
        
            struct in_addr {
                uint32_t   s_addr;           --ip地址
            };

三.网络套接字函数

3.1 soket模型创建流程图

3.2 socket创建套接字

复制代码
int socket(int domain, int type, int protocol);
头文件: #include <sys/types.h>
        #include <sys/socket.h>
功能:创建套接字
参数:
   domain:协议族
     AF_UNIX, AF_LOCAL  本地通信
     AF_INET            ipv4
     AF_INET6            ipv6
   type:套接字类型
     SOCK_STREAM:流式套接字
     SOCK_DGRAM:数据报套接字
   protocol:协议 - 填0 自动匹配底层 ,根据type
      系统默认自动帮助匹配对应协议
       传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
       网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
 返回值:
    成功 文件描述符 0 -> 标准输入  1->标准输出  2->标准出错 
                  3->socket
    失败 -1,更新errno

3.3 bind绑定套接字

复制代码
头文件: #include<sys/types.h>  #include<sys/socket.h> 
         #include<netinet/in.h>  #include<netinet/ip.h>

功能:绑定协议, IP以及port

声明: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:
    sockfd:套接字
    addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,填充对应 结构体-通信当时socket第一个参数确定,需要强转)  
    addrlen:结构体大小   
    
返回值:成功 0   失败-1,更新errno
  
 通用结构体:
  struct sockaddr {
     sa_family_t  sa_family;
     char        sa_data[14];
 }

ipv4通信结构体:
struct sockaddr_in {
    sa_family_t    sin_family;  ----协议族
    in_port_t      sin_port;   ----端口
    struct in_addr sin_addr;     ----ip结构体
};
struct in_addr {
    uint32_t       s_addr;     --ip地址
};

3.4 listen监听

复制代码
int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字
参数:
 sockfd:套接字
 backlog:同一时间可以响应客户端请求链接的最大个数,不能写0.
  不同平台可同时链接的数不同,一般写6-8个

返回值:成功 0   失败-1,更新errno  

3.5 accept阻等待连接

复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
功能:阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
             则accept()函数返回,返回一个用于通信的套接字文件;
参数:
   Sockfd :套接字
   addr: 链接客户端的ip和端口号
      如果不需要关心具体是哪一个客户端,那么可以填NULL;
   addrlen:结构体的大小
     如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值: 
     成功:文件描述符; //用于通信
     失败:-1,更新errno新errno 

3.6 recv

复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据 
参数: 
    sockfd: acceptfd ;
    buf  存放位置
    len  大小
    flags  一般填0,相当于read()函数
    MSG_DONTWAIT  非阻塞
返回值: 
   < 0  失败出错  更新errno
   ==0  表示客户端退出
   >0   成功接收的字节个数

​3.7 connect

复制代码
connect:使用现有的socket与服务器建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket 函数返回值
addr:传入参数。地址结构(服务器的地址结构)
addlen:addr长度
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构,采用"隐式绑定"

3.8 send

复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:指定要发送数据的socket描述符。
buf:指向要发送数据的缓冲区的指针。
len:指定要发送数据的长度(以字节为单位)。
flags:指定发送操作的标志,如`0`或`MSG_DONTWAIT`等。可以在调用时传递特定的标志,也可以用`0`表示无特殊标志。

返回值:
成功: 发送的字节数,
错误:  -1。

需要注意的是,send()

函数并不保证立即将所有数据发送出去,它会尽力将数据发送给远程主机,但在网络条件不理想或发送缓冲区已满的情况下,可能会导致部分数据未能立即发送。

四.案例

利用socket编程。实现一个服务器,客户端发送文本数据给服务器后,服务器将字母大写转小写,小写转大写。转换完成后发送给客户端。客户端输入"quit#"后退出。并且客户端连接后,服务器端打印客户端的ip地址和端口号。

server.c

复制代码
  #include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    //1.socket建立文件描述符
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0)
    {
        perror("socket is err");
    }

    //2.bind绑定服务器ip地址和端口号
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int len = sizeof(caddr);
    int flage = bind(fd, (struct sockaddr *)(&saddr), sizeof(saddr));
    if (flage < 0)
    {
        perror("bind is err");
        return -1;
    }

    //3.listen监听,设置最大同时连接数量
    flage = listen(fd, 5);
    if (flage < 0)
    {
        perror("listen is err");
        return -1;
    }
    printf("listern is ok\n");
    //4.accept等待客户端连接

    int acceptfd = accept(fd, (struct sockaddr *)&caddr, &len);
    if (acceptfd < 0)
    {
        perror("flage is err");
        return -1;
    }
    printf("ip:%s   port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

    //5. 不断读取客服端的内容,大小写转换后,发送给客户端
    char buf[1024] = {0};
    int size = 0;
     while (1)
    {
        flage = recv(acceptfd, buf, sizeof(buf), 0);
        if (flage < 0)
        {
            perror("recv is err");
        }
        else if (flage == 0)
        {
            printf("ip:%s is close\n", inet_ntoa(caddr.sin_addr));
        }
        else
        {
            size = strlen(buf);
            
            for (int i = 0; i < size; ++i)
            {
                if (buf[i] >= 'a' && buf[i] <= 'z')
                    buf[i] = buf[i] + ('A' - 'a');
                else
                    buf[i] = buf[i] + ('a' - 'A');
            }
            
            send(acceptfd, buf, sizeof(buf), 0);
        }
    }
    close(fd);
    return 0;
}

client.c

复制代码
   #include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    //1.socket建立文件描述符
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0)
    {
        perror("socket is err");
    }

    //2.connect连接服务器
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    int flage = connect(fd, (struct sockaddr *)(&saddr), sizeof(saddr));
    if (flage < 0)
    {
        perror("connect is err");
    }

    //3.服务器端不断发送数据,接受服务器转化后的数据
    char buf[1024] = {0};
    while (1)
    {
        //memset(buf,0,sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        if (strncmp(buf,"quit#",5)==0)
        {
            break;
        }
        
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        send(fd, buf, sizeof(buf), 0);
        flage = recv(fd, buf, sizeof(buf), 0);
        if (flage < 0)
        {
            perror("recv is err");
        }
        else
        {
            fprintf(stdout, "%s\n", buf);
        }
    }
    close(fd);
    return 0;
}
相关推荐
BingoGo15 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack15 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
Jony_2 天前
高可用移动网络连接
网络协议
chilix3 天前
Linux 跨网段路由转发配置
网络协议
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php