socket编程函数详解

816 阅读8分钟

socket编程函数详解

一、socket函数

// Linux下面一切都是文件
// 无论是管道,文件还是网络的数据,都是将其当成文件
// 会通过句柄来操作这些文件的读写(将接口统一)
// (LINUX 环境编程)
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain, int type, int protocol);
​
- domain:
    - AF_INET:  这是大多数用来产生socket的协议,使用TCP或者UDP来传输,用IPv4的地址
    - AF_INET6: 与上面的类似,不过是采用IPv6的地址
    // 若是本机的a进程和b进程之间进行通信就无需经过网络了(不会经过网络协议栈)      
    // 那就会在a和b之间,在内部就建立一个通道在内核的高速缓存这一层(通过内核进行通信,效率会更高) 
    // 所以本地间的通信也可以创建一个AF_UNIX         
    - AF_UNIX:  本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台机器上的时候使用
// TCP协议按照顺序保证数据传输的可靠性,同时保证数据的完整性
- type:
    - SOCK_STREAM:(面向流)  这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。使用经常,即socket使用TCP进行传输
    // 常常是stream(用于TCP连接)或dgram(用于UDP服务)
    // 数据报dgram        
    - SOCK_DGRAM:(面向数据报)   (数据用户报文)这个协议是无连接的(即UDP协议),固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。流媒体领域用的多(抖音,直播, 迅雷下载等等),因为它不保证这个数据的可靠性,速度快;但是udp协议是不可靠的,面向无连接, 但是比如迅雷下载在应用层有应用层的协议来保障数据传输的可靠性,应用层协议在udp之上。      
    - SOCK_SEQPACKET: 该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。  
    - SOCK_RAW: socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
    - SOCK_RDM: 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。      
- protocol: 
    - 传0表示使用默认协议。
- 返回值:
    - 成功: 返回指向新创建的socket的文件描述符; 失败: 返回-1, 设置errno
  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。

二、bind函数

服务器会有成千上万的客户端连接,随意变动IP地址都得重新通知客户端,这样就会很麻烦;

一个服务器上还有很多的网卡,多个网络接口上都对应着一个IP地址,不同的网络接口提供不同的服务,绑定的IP地址不一样;

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

1. bind函数的参数解析

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // addrlen就是地址的长度
- sockfd:
    socket 文件描述符 (句柄)
- addr: 
    构造出IP地址+端口号,这个地址要强制转换成sockaddr
- addrlen:
    sizeof(addr)长度
- 返回值:
    成功返回0, 失败返回-1, 设置errno

2. bind函数的作用

bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr(地址)所描述的地址和端口号。

*前面讲过,struct sockaddr 是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而他们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));//清零很关键
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//设置在所有的IP地址上接听
// 这样直到与某个客户端建立了连接时才确定下来用哪个IP地址,端口号为6666
servaddr.sin_port = htons(6666); 

3. listen函数

#include <sys/types.h>
#include <sys/socket.h>int listen(int sockfd, int backlog);
​
- sockfd: 
    socket文件描述符
- backlog:(是指系统还未建立连接之前的等待用户数)
    在Linux系统中,它是指排队等待建立3次握手(TCP连接)的队列的长度
- 返回值:
    成功返回0,失败则返回-1,设置errno

image-20220528163845628.png

典型的服务器程序可以同时服务多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态;listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

4. accept函数

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
​
- sockfd:
    socket文件描述符
- addr:
    传出参数,返回链接客户端地址信息,含IP地址和端口号
- addrlen:
    传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
- 返回值:(区分前面socket函数创建的文件描述符)
    成功返回一个(每一个客户端)新的socket文件描述符(去进行读写),用于和客户端通信,失败返回-1,设置errno(全局的变量)
  • 三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出(保存)客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数设置为NULL,表示对客户端地址不感兴趣,addrlen也可以被设置为NULL。

5. connect函数

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
​
- sockfd:
    socket文件描述符
- addr:
    传入参数,指定服务器端地址信息,含IP地址和端口号
- addrlen:
    传入参数,传入sizeof(addr)大小
- 返回值:
    成功返回0,失败返回-1,设置errno        
  • 只有客户端需要调用connect()连接服务器,connect和bind参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1.

6. socket-backlog设置

#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- backlog:(其实会受到操作系统内核的限制,所以backlog不是设置成多少就可以是多少)
    在Linux系统中,它是指排队等待建立3次握手队列长度

(1)查看系统默认backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog
    
# cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上。

image-20220601111144181.png

(2)改变系统限制的backlog大小

vim /etc/sysctl.conf
    
# 最后添加
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 1024
​
# 保存,然后再执行
sysctl -p

image-20220601111318398.png

  • 如图所示就代表成功了!

image-20220601111737452.png

  • 可以用这样的命令去查询系统参数
sysctl -a | grep max(某个关键词)

7. 出错处理函数

系统调用函数不可能保证把每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障的信息

#include <errno.h>
#include <string.h>
char *strerror(int errnum);
​
- errnum:
    传入参数,错误编号的值,一般取errno的值
- 返回值:
    错误原因
- 1. 创建socket,有可能系统资源不够会报错;
        

(1)绑定错误的IP地址

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>
// 宏定义服务器端口号 
#define SERVER_PORT 6666
//! 错误的IP地址
#define IP "1.1.1.1"
/** 
 *! 收信准备
 *! 1. 准备信箱
 *! 2. 准备标签
 *! 3. 标签写上地址和姓名
 *! 4. 把标签贴到信箱上
 *! 5. 挂置于小区传达室
 *///! 判断传来的字符是不是小写 
int isLowC(char c)
{
    if (c - 'a' >= 0 && c - 'a' <= 25) return 1;
    else return 0;
}
​
//! 封装打印错误并退出函数
//! const 可以同时接受常量参数和变量参数
void perror_exit(const char *des)
{
    fprintf(stderr, "%s error, reason: %s\n", des, strerror(errno));
    //! 其实还应该释放socket的 
    exit(1);
}
int main ()
{
    //! 用整型表示这个信箱sock
    //! 创建信箱 sys/socket.h
    int sock;
​
    //! 定义标签(包括端口号和IP) 在头文件中有 sys/socket.h
    //! sockaddr_in socket 地址
    //! sockaddr_in是系统封装的一个结构体,具体包含了成员变量:sin_family、sin_addr、sin_zero
    /**
     * @brief sockaddr_in结构体
     * 
     *! sin_family指代协议族,在socket编程中只能是AF_INET
     *! sin_port存储端口号(使用网络字节顺序)
     *! sin_addr存储IP地址,使用in_addr这个数据结构
     *! sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
     */
    struct sockaddr_in server_addr;  //! 定义服务器的地址标签
​
    //! socket函数的参数1代表网络通信家族, 这里面涉及到的就是指定TCP/IP
    //! 参数2表示使用TCP协议
    //! 1. 功能:创建一个信箱
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sock) 
    {
        // printf ("create socket error\n");
        // return -1;
        //! 失败会返回-1
        //! 输出标准错误
        //! 只要出错,就会往全局的errno中赋值,把这个值传入strerror函数中即可返回错误的信息
        //! fprintf(stderr, "create socket error, reason: %s\n",strerror(errno));
        // //! 出错了就异常结束
        //! exit(1);
        perror_exit("create sock");
    }
    
    //! 2. 功能:接下来要先清空标签,再写上地址和端口号
    //! 参数1是地址,参数2是要清零的结构有多少字节
    bzero(&server_addr, sizeof(server_addr));
​
    //! 指定协议家族 - 因特网网络协议家族 IPV4
    server_addr.sin_family = AF_INET; 
    //! 对应的IP地址
    //! 要监听哪一个IP地址
    //! 如果要绑定本地所有的IP地址,有几个网卡就有几个IP地址
    //! INADDR_ANY(宏定义) 表示所有的IP地址
    //! htonl把IP地址的字节顺序进行调整  把机器上的字节顺序,调整为网络字节顺序
    //! host to net 
    // server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    inet_pton(AF_INET, IP, &server_addr.sin_addr.s_addr);
    //! 绑定端口号 要将主机字节序列改成网络
    server_addr.sin_port = htons(SERVER_PORT);
​
    //! 3. 将标签贴到收信信箱上
    //! 参数1 信箱; 参数2 struct sockaddr * 指针,强转
    //! 参数3 server_addr 内容大小
    if (-1 == bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)))
    {
        // perror("bind");
        // close(sock);
        // return -1;
        // fprintf(stderr, "bind error, reason: %s\n",strerror(errno));
        // //!出错
        // close(sock);
        // exit(1);
        perror_exit("bind");
​
    }
​
    //! 4. 将信箱挂置在小区传达室上------即监听信号
    //! 参数2即同一时刻允许像我们的服务器端发送连接的客户端的数量
    if (-1 == listen(sock, 6)) 
    {
        // printf ("listen error\n");
        // return -1;
​
        // fprintf(stderr, "listen error, reason: %s\n",strerror(errno));
        // //!出错
        // close(sock);
        // exit(1);
        perror_exit("listen");
    } 
    //! 就等信件了
    printf ("等待客户端的连接\n");
​
    //! 来了信件要怎么接收
    //! 服务器要一直运行
    int done = 1;
    //! 对于读写,有一个新的客户端的套接字
    //! 定义客户端的socket 套接字
    int client_sock; //! 新
    while (done)
    {
        //! 要注意接收的信件是来自谁(accept提供了一个参数)
        struct sockaddr_in client; //! 客户端 标签 sockaddr_in
​
        
​
        char client_ip[64]; //!存储客户端IP
​
        //! 接收从客户端来的内容
        char buf[256];
        int len; //! 接收从客户端来的数据的长度
​
        socklen_t client_addr_len;
        client_addr_len = sizeof(client);
        //! 参数1 从信箱中接收信件 
        //! 参数2 接收客户端的时候也要注意强转
        //! 参数3 这个结构体的长度 client
        //! accept函数会通过参数3返回一些信息,所以参数3这里需要传递一些地址进去
        //! accept函数返回和客户端通信的socket
        //todo 和客户端交流的socket client_sock 
        //todo 从客户端读信回信通过 client_sock
        client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);
​
        if (-1 == client_sock)
        {
            fprintf(stderr, "accept error, reason: %s\n",strerror(errno));
            //!出错
            close(sock);
            exit(1);
        }
        //! 打印客户端地址和端口号
        //! 网络字节转换成主机字节ntop
        //! 网络字节序不是字符串,是字节 逆向的整数表达
        // if (client_sock)
        // {
        printf("client ip : %s\t port : %d\n",
                inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
                ntohs(client.sin_port));
            // );
​
            // break;
        // }
       
        //! 5. 读取客户端发送的数据 客户端对应的socket 是 client_sock
        //! 参数1 是客户端的socket 
        //! 参数2 是接收的数据要存储在哪里 buf 但是buf这里面不是字符串 read函数是不做处理的不添加字符串结束符的'\0'
        len = read(client_sock, buf, sizeof(buf) - 1);
        buf[len] = '\0';
        printf ("receive[%d]: %s\n", len, buf);
        // if (len) break;
​
        for (int i = 0; i < len; ++i)
        {
            // if (isLowC(buf[i]))
            // {
            //     buf[i] = 'A' + buf[i] - 'a';
            // }
            //! 利用自带的函数
            buf[i] = toupper(buf[i]);
        }
        //! 接着要将收到的信息写回去 
        //! 参数1 往客户端的socket(就像一个通道) 写
        len = write(client_sock, buf, len);
        printf("write finished, len: %d\n", len);
        close(client_sock); //! 记得挂机 跟这次的客户端
    }
    close(sock);
    return 0;
}

image-20220601170327944.png

(2)perror

如果只是要将错误输出到标准出错的话,而不是重定向,就可以有perror
#include <errno.h>
#include <string.h>
void perror(const char *s)
    
- s:
    传入参数,自定义的描述
- 返回值:
    无
- 向标准出错stderr输出出错的原因        

\