unix网络编程之SocketAPI基本用法

网络分层模型

预备知识

  • 网络协议
    了解一些基本的网络协议,比如以太网协议TCP/IP协议DNS协议ARP协议等等,这些内容可以看上面给的链接
  • 套接口
    在linux中,套接口即主机+端口。说白了,客户端和服务器要想相互通讯,总需要知道对方的地址吧。怎么表示这个地址呢,就是用套接字。
    常见的套接口地址有:通用套接口地址和ipv4套接口地址。
    通用套接口地址:
    顾名思义,该地址是通用的,所有的套接口地址都可以转为通用套接口地址
    struct sockaddr
    {
    unit8_t sin_len; //4字节
    sa_fmily_t sa_family; //4字节
    char sa_data[14]; //14字节
    }

    ipv4套接口地址:
    该套接口地址是给ipv4用的,当然还有其他套接口地址,只不过ipv4在学习中是常用的。
    struct sockaddr_in {
    uint8_t sin_len; //整个sockaddr_in结构体的长度
    sa_family_t sin_family; //指定该地址家族,在这里必须设为AF_INET(ipv4)
    in_port_t sin_port; //端口号
    struct in_addr sin_addr; //IPV4的地址
    char sin_zero[8]; //暂不使用,一般将其设置为0
    };


    可以发现:通用套接口地址结构和ipv4套接口地址结构,他们大小都是相等的。通用套接口地址中的char sa_data[14],
    就相当于ipv4套接口地址结构中的in_port_t sin_port 和 struct in_addr sin_addr 和 char sin_zero[8]。现在,你可以想明白为什么叫通用套接口地址了吧。
  • 字节序
     大端字节序(Big Endian)
    大端字节序又叫大端对齐,即高字节放在低地址,低字节放在高地址
     小端字节序(Little Endian)
    小端字节序又叫小端对齐,即高字节放在高地址,低字节放在低地址
     主机字节序
    不同的主机有不同的字节序,如x86为小端字节序,Motorola 6800为大端字节序,ARM字节序是可配置的。
     网络字节序
    网络字节序规定为大端字节序
     字节序带来的问题
    我们知道:有些机器是大端对齐,有些是小端对齐。那么,如果一个大端对齐的机器给小端对齐的机器传数据,必将导致错误。所以,这时候我们需要字节序转换函数。在本地设备中,先把本机的字节序转为网络字节序传到服务器,接受方再将网络字节序转为自己的字节序,这样子就保证了不会发生错误。
    字节序转换函数
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
    说明:在上述的函数中,h代表host;n代表network s代表short;l代表long
  • 地址转换函数
    为什么要有地址转换函数
    ip地址是32位的,我们为了表示方便,才写成了十进制点的模式
    所以我们要将ip地质的十进制点模式转为32位的二进制模式
    这样子服务器才会知道ip地址是多少
    地址转换函数
    int inet_aton(const char *cp, struct in_addr *inp); //第一种地址转换函数
    struct in_addr{
    u_int32_t s_addr;
    }
    in_addr_t inet_addr(const char *cp); //第二种地址转换函数
    char *inet_ntoa(struct in_addr in); //第三种地址转换函数
  • 套接字类型
     流式套接字(SOCK_STREAM) //TCP协议
    提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
     数据报式套接字(SOCK_DGRAM) //UDP协议
    提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。
     原始套接字(SOCK_RAW)
  • 字节序和地址转换函数举例
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>

//字节序转换函数 
int main01()
{
    unsigned int data = 0x12345678;
    char *p = &data;

    printf("%x, %x, %x  \n", p[0], p[1], p[2], p[3]);

    if (p[0] == 0x78)
    {
        printf("small \n"); 
    }
    else
    {
        printf("big \n");   
    }
/***************************************
输出:
78,56,34
small
说明是小端对齐,低字节放在低地址 
x86为小段模式
***************************************/

    uint32_t mynetdat = htonl(data); //将主机字节序转为网络字节序
    p = &mynetdat;
    printf("%x, %x, %x  \n", p[0], p[1], p[2], p[3]);
    if (p[0] == 0x78)
    {
        printf("small \n"); 
    }
    else
    {
        printf("big \n");   
    }
/***************************************
输出:
12,34,56
big
说明是大端对齐,低字节放在高地址 
网络字节序规定为大端字节序 
***************************************/
    return 0;
}
/***************************************
为什么要有字节序转换函数:
    我们知道:有些机器是大端对齐,有些是小端对齐。
    那么,如果一个大端对齐的机器给小端对齐的机器传数据,必将导致错误。
    所以,这时候我们需要字节序转换函数 
在本地设备中,先把本机的字节序转为网络字节序传到服务器,
如何接受方再将网络字节序转为自己的字节序,这样子就保证了不会发生错误
***************************************/




//地址转换函数 
int main()
{
    //int inet_aton(const char *cp, struct in_addr *inp);
    //in_addr_t inet_addr(const char *cp);
    //char *inet_ntoa(struct in_addr in);

    in_addr_t myint = inet_addr("192.168.6.222");
    printf("%d\n", myint);

    /*
    struct in_addr{
        u_int32_t   s_addr;
    }
    */
    struct in_addr myaddr;
    inet_aton("192.168.6.222", &myaddr);
    printf("%d\n", myaddr.s_addr);

    printf("%s\n", inet_ntoa(myaddr));

    return 0;
}
/***************************************
为什么要有地址转换函数 :
    tcp协议是32位的
    所以我们要将ip地质的十进制点模式转为32位的二进制模式
    这样子服务器才会知道ip地址是多少
***************************************/

SocketApi基本编程模型

  • CS模型(客户端client/服务器service模式)
    CS模型
    客户和服务器之间要想相互通讯,首先要搭建环境咯,如何搭建呢?
     第一步
    服务器端:socket():这个函数意思是创建一个监听套接字。相当于在家安装电话,等朋友打过来。
    客户端:socket():客户端也有创建一个套接字。相当于在家安装电话,以便可以给朋友打电话。
     第二步
    服务器端:bind():bind函数是用与绑定端口,端口是什么上面链接有介绍。相当于我有那么多朋友,我当然要知道我在等哪个朋友电话咯。
    客户端:不需要做什么
     第三步
    服务器端:listen():listen函数用来监听连接请求。相当于时刻等待电话响起。
    客户端:不需要做什么
     第四步
    服务器端:accept():accept函数用来生成连接套接字,即主动套接字,这个套接字用来进行收发数据。如果此时客户端没有尝试连接(即调用connect函数),那么服务器端将处于阻塞状态。相当于电话响了,我要拿起电话来接听。
    客户端:connect():connect函数用来发送连接请求。相当于给朋友打电话。
    以上四部完成时,环境搭建完毕
     第五步
    服务器端:read(), write():读数据写数据
    客户端:read(), write():读数据写数据
     第六步
    服务器端:关闭监听套接字和关闭主动套接字
    客户端:关闭套接字

  • 简单服务器模型
    这里写图片描述

  • 协议族和套接字类型
    这里写图片描述

  • SocketAPI函数
    服务器端:

#include <sys/socket.h>
int socket(int domain , int type, int protocol);


功能:
创建一个套接字用于通信,被动套接字(监听套接字)
返回值:
成功返回非负整数, 它与文件描述符类似,
我们把它称为套接口描述字,简称套接字。失败返回-1
参数:
domain :指定通信协议族(protocol family)(ipv4,ipv6等等)
type:指定socket类型,流式套接字SOCK_STREAM(TCP协议),数据报套接字SOCK_DGRAM(UDP协议),原始套接字SOCK_RAW
protocol :协议类型(一般填0)
Example:
socket(PF_INET, SOCK_STREAM, 0); //ipv4,TCP协议,0

#include <sys/types.h>          
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);


功能:
设置地址复用,使服务器关闭,客户端未关闭时,可以重启服务器
若没设置地址复用,则服务器关闭,客户端未关闭时,服务器不能重启,必须全部关闭服务器和客户端,然后重新连接
一般放在socket函数之后,bind函数之前
返回值:
成功返回0,不成功返回-1
参数:
sockfd:返回的套接字
level:一般是填SOL_SOCKET
optname:一般是填SO_REUSEADDR
Example:
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt bind\n");
exit(0);
}

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:
绑定一个本地地址到套接字
返回值:
成功返回0,失败返回-1
参数:
sockfd:socket函数返回的套接字
addr:要绑定的地址
//通用套接口地址结构
struct sockaddr
{
unit8_t sin_len;
sa_fmily_t sa_family;
char sa_data[14];
}
//IPv4套接口地址结构 具体请查看:man 7 ip
struct sockaddr_in {
uint8_t sin_len; //整个sockaddr_in结构体的长度
sa_family_t sin_family; //指定该地址家族,在这里必须设为AF_INET(ipv4)
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IPV4的地址
char sin_zero[8]; //暂不使用,一般将其设置为0
};
//IP地址
struct in_addr {
u_int32_t s_addr; //IP地址,32位
};
addrlen:地址长度
Example:
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr(“172.0.0.1”);
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

if ( bind(sockfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr)) < 0 )
{
    perror("func bind: ");
    exit(0);
}
#include <sys/socket.h>
int listen(int sockfd, int backlog);

功能:
监听
返回值:
成功返回0,不成功返回-1
参数:
sockfd:socket函数返回的套接字
backlog: 内核规定的套接字的最大连接个数
进程正在处理一个连接请求的时候,可能还存在其它的连接请求。
因为TCP连接是一个过程,所以可能存在一种半连接的状态,
有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。
如果这个情况出现了,服务器进程希望内核如何处理呢?
内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接
但服务器进程还没有接手处理的连接(还没有调用accept函数的连接),
这样的一个队列内核不可能让其任意大,
所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。

    对于给定的监听套接字,内核要维护两个队列: 
        1 已有客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程。
        2 已完成连接的队列。

Example:
listen(sockfd, SOMAXCONN);

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能:
从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
返回值:
成功返回非负整数,是一个描述符,这个描述即生成的主动套接字
失败返回-1
参数:
sockfd:服务器套接字,socket返回的套接口描述字
addr:将返回对等方的套接字地址
addrlen:返回对等方的套接字地址长度
Example:
struct sockaddr_in perraddr;
socklen_t perrlen;
perrlen = sizeof(perrlen);
accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);

客户端:

#include <sys/socket.h>
int socket(int domain , int type, int protocol);

功能:
创建一个套接字用于通信,被动套接字(监听套接字)
返回值:
成功返回非负整数, 它与文件描述符类似,
我们把它称为套接口描述字,简称套接字。失败返回-1
参数:
domain :指定通信协议族(protocol family)(ipv4,ipv6等等)
type:指定socket类型,流式套接字SOCK_STREAM(TCP协议),数据报套接字SOCK_DGRAM(UDP协议),原始套接字SOCK_RAW
protocol :协议类型(一般填0)
Example:
socket(PF_INET, SOCK_STREAM, 0); //ipv4,TCP协议,0

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:
建立一个连接至addr所指定的套接字
返回值:
成功返回0,失败返回-1
参数:
sockfd:未连接套接字
addr:要连接的套接字地址
addrlen:第二个参数addr长度
Example:
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr(“172.0.0.1”);
connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr));

下面请看具体例子
服务器端:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

int main()
{
    //创建监听套接字
    int sockfd = 0;
    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("func socket: ");
        exit(0);
    }

    int optval = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
    {
        perror("setsockopt bind\n");
        exit(0);
    }

    //绑定端口号
    struct sockaddr_in svraddr;
    svraddr.sin_family = AF_INET;
    svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
    svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

    if ( bind(sockfd, (const struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
    {
        perror("func bind: ");
        exit(0);
    }

    //设置监听
    //一旦调用了listen函数,这个套接字sockfd将变成被动套接字,只能接受,不能发送消息
    //listen管理了两个队列
    if (listen(sockfd, SOMAXCONN) < 0)
    {
        perror("func listen: ");
        exit(0);
    }

    //等待接受,完成连接
    struct sockaddr_in perraddr;
    socklen_t         perrlen;
    perrlen = sizeof(perrlen);
    unsigned int conn; 
    conn = accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);//accept返回一个新的连接,这个连接时一个主动套接字
    if (conn == -1)
    {
        perror("func accept: ");
        exit(0);
    }
    printf("perradd:%s, perrport:%d\n", inet_ntoa(perraddr.sin_addr), ntohs(perraddr.sin_port)); //打印客户端ip地址,端口号

    // 收发数据
    char revbuf[1024] = {0};
    while (1)
    {
        int ret = read(conn, revbuf, sizeof(revbuf));
        if (ret == 0)
        {
            //如果在读的过程中,对方已经关闭,tcp/ip协议会返回一个数据包
            printf("client already close\n");
            exit(0);
        }
        else if (ret < 0)
        {
            perror("read fail:");
            exit(0);
        }
        fputs(revbuf, stdout); //服务器端打印收到的数据
        write(conn, revbuf, ret); //服务器端回发报文
        memset(revbuf, 0, sizeof(revbuf));
    }

    close(conn);
    close(sockfd);

    return 0;
}

客户端

int main()
{
    int sockfd = 0;
    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("func socket: ");
        exit(0);
    }

    struct sockaddr_in svraddr;
    svraddr.sin_family = AF_INET;
    svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
    svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

    if ( connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
    {
        perror("func connect: ");
        exit(0);
    }

    char revbuf[1024] = {0};
    char sendbuf[1024] = {0};
    while( fgets(sendbuf, sizeof(sendbuf), stdin) != NULL )
    {
        write(sockfd, sendbuf, strlen(sendbuf)); //向服务器写数据
        read(sockfd, revbuf, sizeof(revbuf)); //从服务器读数据
        fputs(revbuf, stdout);

        memset(revbuf, 0, sizeof(revbuf));
        memset(sendbuf, 0, sizeof(sendbuf));
    }

    close(sockfd);

    return 0;
}

为了实现多用户使用服务器,我们可以用fork方法,每连接一次便fork一次,让一个进程去管理一个用户,示例如下:

服务器端:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

int main()
{
    //创建监听套接字
    int sockfd = 0;
    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("func socket: ");
        exit(0);
    }

    int optval = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
    {
        perror("setsockopt bind\n");
        exit(0);
    }

    //绑定端口号
    struct sockaddr_in svraddr;
    svraddr.sin_family = AF_INET;
    svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
    svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

    if ( bind(sockfd, (const struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
    {
        perror("func bind: ");
        exit(0);
    }

    //设置监听
    //一旦调用了listen函数,这个套接字sockfd将变成被动套接字,只能接受,不能发送消息
    //listen管理了两个队列
    if (listen(sockfd, SOMAXCONN) < 0)
    {
        perror("func listen: ");
        exit(0);
    }

    //等待接受,完成连接
    // 收发数据
    struct sockaddr_in perraddr;
    socklen_t         perrlen;
    perrlen = sizeof(perrlen);
    unsigned int conn; 

    while (1)
    {
        conn = accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);//accept返回一个新的连接,这个连接时一个主动套接字
        if (conn == -1)
        {
            perror("func accept: ");
            exit(0);
        }
        printf("perradd:%s, perrport:%d\n", inet_ntoa(perraddr.sin_addr), ntohs(perraddr.sin_port)); //打印客户端ip地址,端口号

        //每来一个连接,fork一次
        int pid = fork();
        if (pid == 0) //子进程用于收发数据
        {
            while(1)
            {
                close(sockfd); //子进程不需要监听
                char revbuf[1024] = {0};
                int ret = read(conn, revbuf, sizeof(revbuf));
                if (ret == 0)
                {
                    //如果在读的过程中,对方已经关闭,tcp/ip协议会返回一个数据包
                    printf("client already close\n");
                    exit(0);
                }
                else if (ret < 0)
                {
                    perror("read fail:");
                    exit(0);
                }
                fputs(revbuf, stdout); //服务器端打印收到的数据
                write(conn, revbuf, ret); //服务器端回发报文
                memset(revbuf, 0, sizeof(revbuf));
            }

        }
        else if (pid > 0) //父进程只用于监听,关闭主动套接字
        {
            close(conn);
        }
        else
        {
            printf("fork fail\n");
            close(conn);
            close(sockfd);
            exit(0);
        }   
    }

    return 0;
}

客户端

int main()
{
    int sockfd = 0;
    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("func socket: ");
        exit(0);
    }

    struct sockaddr_in svraddr;
    svraddr.sin_family = AF_INET;
    svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
    svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

    if ( connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
    {
        perror("func connect: ");
        exit(0);
    }

    char revbuf[1024] = {0};
    char sendbuf[1024] = {0};
    while( fgets(sendbuf, sizeof(sendbuf), stdin) != NULL )
    {
        write(sockfd, sendbuf, strlen(sendbuf)); //向服务器写数据
        read(sockfd, revbuf, sizeof(revbuf)); //从服务器读数据
        fputs(revbuf, stdout);

        memset(revbuf, 0, sizeof(revbuf));
        memset(sendbuf, 0, sizeof(sendbuf));
    }

    close(sockfd);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值