unix网络编程之SocketAPI基本用法

来源:互联网 发布:染色体图像分析软件 编辑:程序博客网 时间:2024/06/08 09:32

网络分层模型

  • 这一部分涉及内容比较多,分享一个链接,内容通俗易懂,写得很不错,各位可以先去看看,大概了解了解
    互联网协议入门(通俗易懂的网络协议层次结构讲解)

预备知识

  • 网络协议
    了解一些基本的网络协议,比如以太网协议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,34small说明是小端对齐,低字节放在低地址 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,56big说明是大端对齐,低字节放在高地址 网络字节序规定为大端字节序 ***************************************/    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;}
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 微信手机登录收不到验证码怎么办 淘宝店的宝贝没有尺码选项怎么办 淘宝账号被保护要自助开通怎么办 小米平板经常出现系统无响应怎么办 淘宝买家被取消运费险了怎么办 新开的淘宝直播店没有流量怎么办 淘宝买的东西一直不发货怎么办 在淘宝上买了东西不发货怎么办 宝贝好几天拉一次吃的也少怎么办 淘宝快递正在运输途中想退货怎么办 淘宝买的东西退货商家拒收怎么办 手机屛上出现了微信图标怎么办 手机用了两年了反应太慢怎么办 魅族手机显示手机已锁定怎么办 为什么微信注册要安全验证码怎么办 微信帐号异常无法领取红包怎么办 帮朋友代付在支付宝被骗怎么办 商家说未收到货拒绝退款怎么办 淘宝商家拒绝退款怎么办还没收货的 拼多多点错确认收货了怎么办 被别人用菜刀砍伤没钱看病怎么办 东京下了订单但不发货怎么办 绑定卷皮钱包的手机号码丢了怎么办 小孩回奶在垫的被子上发霉了怎么办 2个月宝宝不喝母乳只喝奶瓶怎么办 我的扣扣被盗了朋友别被骗了怎么办 我买的股票退市了我的钱怎么办啊 在美食林被门口买宝石的骗了怎么办 在商场买的彪马鞋子皮子裂了怎么办 手机换号了京东钱包里的余额怎么办 寄报销发票给顺丰快递搞丢了怎么办 物流显示揽件但把快递弄丢了怎么办 在李宁商城上买的东西丢了怎么办 我的货发物流都过了好几天怎么办 运动鞋子买小了一码有些挤脚怎么办 媳妇先动手打我我又打媳妇了怎么办 京东商城买个电视没验收破了怎么办 钱充给波克城市游戏还不能玩怎么办 我的魅族账号密保问题忘记了怎么办 在手机店买手机买贵了被骗了怎么办 信翼4g上网宝登录密码忘了怎么办