Linux C socket编程(1)

socket编程API


创建套接字–用于网络传输
  • 在Linux操作系统中,要实现socket通信,通信双方都需要建立各自的socket对象,在应用层,socket对象是一种特殊的文件描述符,可以使用I/O系统调用(read/write)来读写,socket()函数用于创建socket,其函数声明如下:
#include <sys/socket.h>
int socket(int domain, //协议域
                       //AF_INET(IPv4)
                       //AF_INET6(IPv6)
                       //AF_UNIX(本地套接字)
            int type, //套接字类型
                      //流式SOCK_STREAM (TCB)
                      //数据报SOCK_DGRAM (UDP)
                      //原始SOCK_RAM
            int protocol //协议类型,一般写0
            );
返回值:套接字描述符(文件描述符),简称为套接字(可读可写)
  • 此函数如果执行成功,将返回一个打开的socket文件描述符,此时,该socket对象没有绑定任何IP信息,还不能进行通信,如果执行失败,返回-1
绑定本地IP地址与端口
  • 使用socket()创建的socket是没有任何约束的,它没有与具体的端口号相关联,在服务器端,需要使用bind函数绑定该套接字。bind()函数声明如下:
#include <sys/socket.h>
int bind(int sockfd,                 //刚才打开的套接字(用于绑定本地IP信息的文件描述符)
        const struct sockaddr *addr, //套接字地址(必须含有IP地址和端口号)
        ocklen_t addrlen             //地址的长度(一般有sizeof求得)
        );
  • 第二个参数addr:是一个指向sockaddr结构的指针,标识绑定的本地地址信息,如果是IP信息,则要求IP地址必须为本地IP地址,端口必须为一个未占用的本地端口。sockaddr数据结构定义如下:
struct sockaddr{
    sa_family_t sa_family;  /* address family,AF_xxx */  //协议簇
    char sa_data[14];       /* 14 bytes of protocol address */  //协议地址
}
  • struct sockaddr只是提供地址类型规范,根据不同的应用,sockaddr需要选用不同的类型。
  • 例如IPv4网络通信,socket需要与本机可用的IP地址和端口号绑定,因此,sockaddr结构体应该选用一下定义:
struct sockaddr_in {
    sa_family_t    sin_family;   /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */

    struct in_addr sin_addr;   /* internet address */
};

//struct in_addr为32位IP地址,具体定义如下:
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};
  • 其中,端口对任何一个socket都是唯一的,唯一的端口号可以区分本地唯一的应用程序,因此,socket所绑定的端口号不能与其他应用程序重复,小于1024的端口号为系统保留,用户应用程序不能随便使用

setsockopt()–获取或者设置与某个套接字关联的选项,函数定义如下

#include <sys/socket.h>
int setsockopt(int sockfd, //将要被设置或者获取选项的套接字
                int level, //选项所在的协议层
                int optname, //需要访问的选项名
                void *optval, //指向包含新选项值的缓冲
                socklen_t *optlen //现选项的长度
                );
  • level:SOL_SOCKET(通用套接字选项)、IPPROTO_IP(IP选项)、IPPROTO_TCP(TCP选项)…..
  • optname:SO_REUSEADDR(允许重用本地地址)、SO_REUSEPORT(允许重用本地端口)、…..
  • optval:若想在关闭套接字后继续重用该socket,可设置为1.
监听网络
  • 绑定了IP地址和端口号的socket对象还不能进行TCP方式通信,因为当前还没有能力监听网络请求。因此,对于面向连接的应用来说,服务器端需要调用listen函数使该socket对象监听网络。仅由TCP服务器调用。将一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求,函数声明如下:
 #include <sys/socket.h>
int listen(int sockfd, //刚才创建的套接字
            int backlog //等待队列的最大个数
            );
  • listen函数将绑定的socket文件描述符变为监听套接字,此时服务器已经准备好接收客户端的连接请求了
客户端发起连接
  • 如果服务器已经监听网络,且客户端创建了socket对象,则客户端可以使用connect()函数与服务器端建立连接了。connect()函数声明如下:
#include<sys/socket.h>
int connect(int sockfd,                     //socket返回的文件描述符
            const struct sockaddr *servaddr,//目的主机地址(IP和端口号)
            socklen_t addrlen);             //该地址的长度
  • 如果执行成功,此函数将于地址为servaddr的服务器建立连接,并返回0,失败返回-1
服务器接收连接
  • 如果服务器监听到客户端的连接请求,则需要调用accept()函数接受请求,如果没有监听到客户端的连接请求,则此函数处于阻塞状态。accept()函数声明如下:
#include <sys/socket.h>
int accept(int sockfd,             //监听网络后的socket文件描述符
            struct sockaddr *addr, //哪个客户端连接我,我就记录此客户端的地址()
            socklen_t *addrlen     //传入是告诉接收方分配的空间多大,传出是告诉对方实际占用空间多大
            );

返回值:新的套接字,用于与客户端进行通信(传出客户端的地址和端口号)
  • 如果连接成功,返回新的文件描述符以标识该连接,从而使原来的文件描述符可以继续监听网络等待新的连接,这样便可以实现多客户端。如果执行失败,返回-1
读/写socket对象
  • read函数是负责从socket对象中读取内容,当读取成功时,read返回实际读取到的字节数,如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。
  • write函数将buf中的nbytes字节内容写入到socket对象中,成功返回写的字节数,失败返回-1.并设置errno变量。
  • 函数声明如下:
#include <unistd.h>
ssize_t write(int fd, 
            const void *buf,
            size_t count
            );

ssize_t read(int fd, 
            void *buf, 
            size_t count
            );
TCP发送/接收数据
  • Linux提供send()和recv()函数来专门实现面向连接的socket对象读写操作。
  • send()函数用来发送数据,函数声明如下:
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd,            //目标socket对象
            const void *buf,        //欲发送数据位置
            size_t len,             //数据大小
            int flags               //置0,则与write()行为一致;MSG_PEEK,查看外来消息,系统不丢弃看到的数据;……
            );
  • 如果执行成功,则返回发送数据的大小,失败返回-1
  • recv()函数用来接收数据,其将从socketfd中读取n个字节到buf中,函数声明如下:
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 如果执行成功,则返回接收数据的大小,失败返回-1
关闭socket对象
  • 通信结束后,关闭socket对象,使用close()函数
#include<unistd.h>  
int close(int fd);  
获取socket本地及对端信息
  • 使用getsockname()函数将获取一个套接字(这个套接字至少完成了绑定本地IP地址)的本地地址。如果成功返回0,失败返回-1。函数声明如下:
#include <sys/socket.h>

int getsockname(int sockfd,              //想要读取信息的socket文件描述符
                struct sockaddr *addr,   //存储地址的内存空间的地址
                socklen_t *addrlen       //存储地址的内存空间的大小
                );
  • 函数使用示例:
struct sockaddr_in test;
getsockname(fd,(struct sockaddr*)&test,&size);
printf("ip=%s,port=%d\n",inet_ntoa(test.sin_addr),ntohs(test.sin_port));
主机字节顺序与网络字节顺序的转
  • htonl:将主机的unsignedlong值转换成网络字节顺序(32位)(一般主机跟网络上传输的字节顺序是不通的,分大小端),函数返回一个网络字节顺序的数字
  • 记忆这类函数,主要看前面的n和后面的hl。n代表网络,h代表主机host,l代表long的长度,还有相对应的s代表16位的short
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
IPv4地址转换
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//将点分十进制的字符串转换为32位网络字节顺序的IP信息
in_addr_t inet_addr(const char *cp);

//将点分十进制的字符串转换为32为主机字节序的IP信息
in_addr_t inet_network(const char *cp);

//将点分十进制的字符串IP信息准换成32位的网络字节序
int inet_aton(const char *cp, struct in_addr *inp);

//将32位的网络字节序的IP信息转换为点分十进制的字符串形式
char *inet_ntoa(struct in_addr in);

简易服务器端-客户端实现


  • 上面我们介绍了socket编程的常用接口,下面我们将应用上面介绍的接口实现一个简单的服务端-客户端模型
服务器端
  • 创建套接字(socket) -> 绑定(bind) -> 监听(listen) -> 等待客户端连接(accept) -> 读取客户端发送过来的数据(read) -> 回复客户端(write) -> 关闭(close)
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>

int main(int argc,char* argv[])
{
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    if(argv[1])
        addr.sin_addr.s_addr = inet_addr(argv[1]);
    else 
        addr.sin_addr.s_addr = INADDR_ANY; //否则设置本机任意地址

    int ret = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        return -1;
    }
    printf("本地 ip = %s and port = %d 绑定成功...\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
    ret = listen(fd,10);
    if(ret == -1)
    {
        perror("listen");
        return -1;
    }
    printf("等待客户端连接...\n");
    //服务器接收客户端的连接
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int newfd = accept(fd,(struct sockaddr*)&cliaddr,&len);
    if(newfd == -1)
    {
        perror("accept");
        return -1;
    }
    printf("ip = %s and port = %d 的客户端已连接我方服务器...\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
    //已经有客户端连接到我了,提示客户端输入消息,读取客户端写的数据并发送写回给客户端
    //一个客户端可以和服务端进行多次交互
    while(1)
    {
        char buf[1024]={};    
        ssize_t r = recv(newfd,buf,sizeof(buf),0);
        if(r == -1)
        {
            perror("recv");
            return -1;
        }
        printf("%s",buf);
        //将服务器读到的信息写回给客户端
        r = write(newfd,buf,sizeof(buf));
        if(r == -1)
        {
            perror("write");
            return -1;
        }
    }
    close(newfd);
    close(fd);
    return 0;
}

客户端
  • 创建套接字(socket) -> 连接服务器(connect) -> 向服务器端写入数据(write) -> 读取服务器端的回复(read) -> 关闭(close)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
int main(int argc,char* argv[])
{
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }

    //和服务端进行连接
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    inet_aton(argv[1],(struct in_addr*)&addr.sin_addr.s_addr);

    int r = connect(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(r == -1)
    {
        perror("connect");
        return -1;
    }
    printf("已连接到 ip = %s port = %d 的服务器,准备通讯\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));

    char buf[1024]={};
    while(fgets(buf,1024,stdin))
    {
        if(!strncasecmp(buf,"quit",4))
        {
            printf("本客户端要退出了...\n");
            break;
        }
        ssize_t r = send(fd,buf,sizeof(buf),0);
        if(r == -1)
        {
            perror("send");
            return -1;
        }

        r = read(fd,buf,sizeof(buf));
        if(r == -1)
        {
            perror("read");
            return -1;
        }
        printf("%s",buf);
        memset(buf,0x00,sizeof(buf));
    }
    close(fd);
    printf("已和 ip = %s and port = %d 的服务器断开连接...\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
    return 0;
}
  • 执行结果如下:
    这里写图片描述

多进程版本的服务端-客户端模型


  • 对上面模型进行改进,使其达到一个服务器可以同时与多个客户端进行叫交互
  • 针对于此,我们通过多进程来实现多个客户端并行的情况,父进程阻塞等待客户端的来连接,子进程来与服务器进行交互
  • 实现代码如下:

服务端代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void handle_cli(int newfd,struct sockaddr_in cliaddr)
{
    while(1)
    {
        char buf[1024]={};
        ssize_t r = recv(newfd,buf,sizeof(buf),0);
        if(r == -1)
        {
            perror("recv");
            exit(0);
        }
        if(!strncasecmp(buf,"quit",4))
        {
            printf("客户端 ip = %s and port = %d 已从本端断开...\n",
                   inet_ntoa((struct in_addr)cliaddr.sin_addr),ntohs(cliaddr.sin_port));
            close(newfd);
            exit(0);
        }
        printf("客户端 ip = %s and port = %d 说:%s",inet_ntoa((struct in_addr)cliaddr.sin_addr),ntohs(cliaddr.sin_port),buf);
        r = write(newfd,buf,sizeof(buf));
        if(r == -1)
        {
            perror("write");
            exit(0);
        }
    }
}

int main(int argc,char* argv[])
{
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_addr.s_addr = inet_addr(argv[1]);

    int ret = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        return -1;
    }
    //绑定本地端口号成功
    printf("绑定本地 ip = %s and port = %d 成功...\n",inet_ntoa((struct in_addr)addr.sin_addr),ntohs(addr.sin_port));

    ret = listen(fd,10);
    if(ret == -1)
    {
        perror("listen");
        return -1;
    }
    printf("等待客户端的连接...\n");

    //通过父子进程来实现服务器与客户端一对多的模型(可以多次进行通信)
    //让父进程阻塞等待客户端的到来,子进程处理与服务器的交互
    while(1)
    {
        struct sockaddr_in cliaddr;
        socklen_t len = sizeof(cliaddr);

        int newfd = accept(fd,(struct sockaddr*)&cliaddr,&len);
        if(newfd == -1)
        {
            perror("accept");
            return -1;
        }
        pid_t pid;
        pid = fork();
        if(pid == -1)
        {
            perror("fork()");
            return -1;
        }else if(pid > 0)//父进程
        {
            close(newfd);
            continue;
        }else if(pid == 0)//子进程
        {
            close(fd);
            printf("客户端 ip = %s and port = %d 已与我方服务器连接,准备通讯...\n",
                   inet_ntoa((struct in_addr)cliaddr.sin_addr),ntohs(cliaddr.sin_port));
            handle_cli(newfd,cliaddr);
            return -1;
        }

        close(newfd);

    }
    close(fd);
    return 0;
}

客户端代码如下:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc,char* argv[])
{
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    addr.sin_addr.s_addr = inet_addr(argv[1]);

    int ret = connect(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1)
    {
        perror("connect");
        return -1;
    }
    printf("成功与服务器 ip = %s and port = %d 连接,准备进行通讯...\n",
           inet_ntoa((struct in_addr)addr.sin_addr),ntohs(addr.sin_port));

    char buf[1024] = {};
    while(fgets(buf,1024,stdin))
    {
        if(!strncasecmp(buf,"quit",4))
        {
            send(fd,buf,sizeof(buf),0);
            close(fd);
            return -1;
        }
        int r = send(fd,buf,sizeof(buf),0);
        if(r == -1)
        {
            perror("send");
            return -1;
        }

        r = read(fd,buf,sizeof(buf));
        if(r == -1)
        {
            perror("read");
            return -1;
        }
        printf("%s",buf);
        memset(buf,0x00,sizeof(buf));
    }
    close(fd);
    return 0;
}
  • 程序运行结果如下所示:
    这里写图片描述
    这里写图片描述

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/79669765