Linux下socketAPI的使用,实现udp和tcp通信

28 篇文章 0 订阅
5 篇文章 1 订阅

目录

一.常用的socketAPI

1.socket()-创建套接字

2.bind()-绑定端口号

sockaddr结构

3.recvfrom()-接收数据的函数

4.sendto() 

5.listen函数-TCP

6.accept函数-TCP

7.connect函数-TCP

 二.实现简易的UDP通信

1.目标:

2.大致思路:

server:

client:

3.代码实现

4.运行结果

三.实现简易的TCP通信

1.目标

2.大致思路

server

client

3.代码实现

4.运行结果


一.常用的socketAPI

1.socket()-创建套接字

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

int socket(int domain, int type, int protocol);

参数:

  1. domain:域,指套接字的种类,即你希望操作系统给你提供哪种服务,有很多种,一般我们用的最多的是AF_INET
  2. type:套接字的类型,也有很多种,用的最多的是SOCK_SRREAM和SOCK_DGRAM
  3. protocol:直接设为0,如果前两个参数确定,第三个参数也就确定了。

返回值:一个文件描述符,执行socket()函数创建套接字的时候,相当于把网络以文件的形式打开了。创建失败就返回小于0的数


2.bind()-绑定端口号

// 绑定端口号 (TCP/UDP, 服务器)

int bind(int socket, const struct sockaddr *address,

 socklen_t address_len);

参数:

  1. socket:就是socket函数返回的文件描述符
  2. address:一个sockaddr类型的结构体指针,我们需要在其中指明一些信息(用哪种类型的网络服务,IP地址,端口号之类的信息),这里我们指明三个参数:sin_family   sin_port   sin_addrs.addr
  3. address_len:前面address结构体的字节数

返回值:如果绑定成功返回0,绑定失败返回-1

注意:bind(sock, (struct sockaddr*)&local, sizeof(local))传入第二个参数的时候需要进行强转

sockaddr结构

        网络通信的标准方式有很多种,比如基于ip的网络通信,AF_INET,原始套接字,域间套接字。有不同种类的套接字,但是网络标准把它们统一化了,所以就有了sockaddr这一结构。

        我们传入不同套接字的socketaddr结构的时候,把它们都强转成统一的sockaddr类型,然后根据前面的16位地址类型,判断是哪一种套接字。(就像是c++里面的多态,sockaddr就像是用C语言实现的父类)

        IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。这里我一般使用的sin_family16位地址类型使用是AF_INET。


3.recvfrom()-接收数据的函数

服务器提供的服务一般都是死循环,就是一直提供服务。

参数:

  1. sockfd:传入套接字的文件描述符
  2. buf:读入的数据放在这个缓冲区中
  3. len:缓冲区的长度
  4. flags:读的方式,默认为0就可以
  5. src_addr:输入输出型参数,用于保存发送方的套接字信息
  6. addrlen:作用同上,用于保存套接字的长度

5,6两个参数都是用来保存客户端的socket信息的

        输入:提供一段空间

        输出的时候用来表明是谁给服务器发送的消息

返回值:返回读取到多少个字节,读取失败就返回-1

        注意:src_addr是struct sockaddr类型的,我们需要使用的是struct_sockaddr_in类型的,所以我们在将其作为参数传入的时候,需要强转成struct sockaddr类型的。


4.sendto() 

参数:

  1. sockfd:传入套接字的文件描述符
  2. buf:要发送的数据放在这个缓冲区中
  3. len:要发送数据的长度(如果设为缓冲区的长度就是发送缓冲区内所有的信息)
  4. flags:发送的方式,默认为0就可以
  5. dest_addr:输入输出型参数,用于保存目的进程的套接字信息
  6. addrlen:作用同上,用于保存套接字的长度

5,6两个参数都是用来保存目的进程的socket信息的

        输入:提供一段空间

        输出的时候用来表明是往哪个进程发信息(目的进程的套接字信息)

返回值:返回发送了多少个字节,读取失败就返回-1

         注意:这里的recvfrom和sendto实际上对应的是read和write,本质上都是往文件中写数据。


5.listen函数-TCP

  • 监听函数,将当前服务进程设置为可接收连接的状态。
  • 参数:sockfd:监听套接字的fd;backlog:全连接队列的长度-1
  • 返回值:成功返回0,失败返回-1

6.accept函数-TCP

用于接收连接用

参数:

  1. sockfd:监听套接字(就像鱼庄的拉客少年张三),这个套接字的作用是监听
  2. addr:输入输出型参数,用于保存发送方的套接字信息
  3. addrlen:输入输出型参数,用于保存发送方的套接字信息

返回值:如果接收成功,返回非0的整数,这个整数是一个文件描述符,也算是一个套接字,这个套接字的作用是提供确切的服务


7.connect函数-TCP

参数:同上面的accept

返回值:成功连接返回0,失败返回-1


 二.实现简易的UDP通信

1.目标:

        实现一个简单的UDP通信服务,client给server发送消息,server能够接收到消息并处理,返还结果给client,我这里实现的是client发送字符串指令,执行server服务器上的命令行操作,然后server将结果返回给client

2.大致思路:

server:

1.首先创建套接字,打开网络文件-socket接口

2.然后给该服务器绑定端口和ip(特殊处理)--bind接口

3.提供服务,接收客户端传过来的数据--recvfrom函数

4.响应客户端,将数据发送回客户端--sendto函数

注意点:

1.在给当前服务进程绑定端口号的时候,需要调用htons函数,因为这里的端口号是主机上的序列,需要转换成网络字节序

2.在给当前服务进程绑定IP地址的时候,不能指定绑死一个IP,因为当前服务端可能配置有多个IP地址,我们需要的不是从某个IP地址传过来的数据,我们需要的是从所有IP地址传过来,来访问当前port对应服务进程的数据,所以我们指定IP地址的时候,用local.sin_addr.s_addr = INADDR_ANY;表示可以接收从当前服务器上任一IP地址传过来的数据。

3.调用recvfrom函数接收数据的时候,要传入两个参数用来保存客户端方的socket信息,用于之后sendto函数,根据这里得到的socket信息,向客户端发送数据。


client:

1.创建套接字,打开网络文件--socket接口

2.从命令行参数中获取要访问的服务端的ip地址和port端口号,设置到自己定义的struct sockaddr_in的结构体中。注意int main(int argc, char *argv[]),命令行参数总共有三个,我们在命令行运行的时候要给出三个参数./udp_client   server_ip   server_port,如果给出的命令行参数不足3个,则打印出提示语句

3.调用sendto函数把数据通过套接字发送给服务端,注意将带有服务端socket信息的sockaddr_in结构体传入

4.调用recvfrom函数接收服务端返回的数据,可以定义一个sockaddr_in类型的tmp用于保存服务端的socket信息(实际上在命令行参数里面已经给出了,这里充当的是一个占位符,如果之后要用到,比如从别的主机接收数据,那时候再使用)

注意点:

1.客户端不需要显示的bind端口号。首先,客户端要与服务端通信,必须要有socket,也就是IP地址+port端口号,但是,客户端不需要显示的bind!一旦显示bind,就必须明确,client要和哪一个port关联。然而如果我们写死与哪个端口号关联,比如8080,这个端口号可能已经被其他应用程序占用,所以我们不需要显示地bind端口号,只要有空闲的端口号,用就行了(客户端能跑起来就行)。client的端口号一般由OS自动bind,在client发送数据的时候,OS会自动bind,采用的是随机端口的方式。

        服务端需要绑定端口号,而且必须明确,不能改变。因为要一直提供确切的服务。在服务端理论上也会有端口号互相冲突的情况,但是服务端的接口和port之间一般会进行系统的管理,所以一般都不会冲突。


3.代码实现

server.cc

/*
 * @Author: zebra
 * @Date: 2023-02-03 23:11:43
 * @LastEditTime: 2023-02-04 13:55:27
 * @LastEditors: zebra
 * @Description: 
 * @FilePath: /cpp/learning/f1_blog/a11_socketAPI/udp_server.cc
 * by zebra
 */
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const uint16_t port = 8080;

int main()
{
    // 1. 创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket create error: " << errno << std::endl;
        return 1;
    }

    // 2. 绑定IP和端口号
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port); // 此处的端口号,是前面定义的一个变量,属于主机上的一个变量,是主机序列,需要转换成网络字节序
    local.sin_addr.s_addr = INADDR_ANY;  //一个服务器上可能有多张网卡,对应多个IP,因此我们不能绑死一个IP地址,而是将任一IP同一个port接收到的数据都拿上来。

    if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error : " << errno << std::endl;
        return 2;
    }

    // 3. 提供服务
    bool flag = false;
#define NUM 1024

    char buffer[NUM];
    while (!flag)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
		
        ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (cnt > 0)
		//cnt表示收到了多少字节的数据
        {
            buffer[cnt] = 0; //0=='\0',我们收到的是字符串数据
            FILE *fp = popen(buffer, "r");  //执行buffer中的指令

            std::string ans;
            char line[1024] = {0};
            while(fgets(line, sizeof(line), fp) != NULL){
                ans += line;
            }

            pclose(fp);
            std::cout << "client message get! content: " << buffer << std::endl;
            
            sendto(sock, ans.c_str(), ans.size(), 0, (struct sockaddr *)&peer, len);
        }
    }
    return 0;
}

client:

/*
 * @Author: zebra
 * @Date: 2023-02-03 23:11:48
 * @LastEditTime: 2023-02-04 13:54:54
 * @LastEditors: zebra
 * @Description: 
 * @FilePath: /cpp/learning/f1_blog/a11_socketAPI/udp_client.cc
 */
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void Usage(std::string proc)
{
    std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    // 1. 创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error : " << errno << std::endl;
        return 1;
    }

    //client不需要显示bind绑定ip和port,OS会自动帮我们绑定
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    // 2.使用服务,发起请求
    while (1)
    {
        std::cout << "client: ";
        char line[1024];
        fgets(line, sizeof(line), stdin);

        sendto(sock, line, strlen(line), 0, (struct sockaddr*)&server, sizeof(server));

        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        char buffer[1024];
        ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
        if(cnt > 0)
        {
            buffer[cnt] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

4.运行结果

client:

server:

 


三.实现简易的TCP通信

1.目标

        建立简易的TCP通信,client发送消息给server,server接收到消息并处理,然后返回给client。

2.大致思路

server

1.创建套接字,和UDP同理

2.bind服务的IP和port,和UDP同理

3.tcp是面向连接的,所以一定有人主动建立(客户端,需要服务,要服务所以主动请求建立连接),一定有人被动接受连接(服务器,提供服务)。因此在服务端,设置套接字为Listen状态,本质是让当前服务进程允许其他用户来连接

4.调用accept函数接收连接,这里返回的文件描述符是真正提供服务的套接字

5.如果接收连接成功,提供服务

        由于tcp连接是流式套接,所以可以像对文件一样操作套接字(这里指的是提供服务的套接字,不是listen监听套接字)

6.当有连接建立成功的时候,创建一个线程来为client提供服务。

        1).pthread_create创建一个线程来执行任务,进入我们定义的handlerRequest函数,注意要把new_sock的指针作为参数传过去(这里要创建一个新的int对象)

        2).在处理函数内部首先调用pthread_detach方法分离线程,让线程结束后自动释放资源。这样就不需要主线程来join了(主线程来join那就变成串行了)

        3).拿出传入的套接字,然后释放传入的参数资源(因为我们传入参数的时候是创建了一个int对象的,我们在函数里面拿到对应的值以后,要把int对象的空间释放,避免资源浪费)

        4).提供服务,关闭new_sock套接字


client

1.可以在命令行指定要访问服务的ip和port,注意从命令行参数argv里面拿出来的port是字符串,我们需要的是uint16_t类型的,所以需要使用atoi函数或者stoi函数。

2.创建套接字,同理

3.client无需显示绑定端口号,让OS自动帮我们绑定即可(如果你显示绑定,其他client进程随机绑定,随机到你显示绑定的这个port,那你显示绑定的client进程就无法运行了)

4.往sockaddr_in里面写入要访问服务进程的ip,port,使用的服务等等信息。

        1).注意我们的port是主机序列,是小端,要转成网络序列。ip地址是字符串(htoi)

        2).我们的ip是点分十进制的字符串风格的IP,要转化成为4字节IP(inet_addr)

5.通过connect函数连接服务器,对应的就是accept接收

6.进行业务请求


3.代码实现

server.cc

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void ServiceIO(int new_sock)
{
    while (true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = read(new_sock, buffer, sizeof(buffer) - 1);
        // 调用read函数对提供服务的new_socket像文件一样操作
        if (s > 0)
        {
            buffer[s] = 0; // 将获取的内容当成字符串
            std::cout << "client message get! content: " << buffer << std::endl;

            std::string echo_string = "server get your message: ";
            echo_string += buffer;

            write(new_sock, echo_string.c_str(), echo_string.size());
        }
        else if (s == 0)
        {
            // 如果read返回0,说明client端关闭了(socket连接关闭的时候会发送一个FIN消息,此时read返回0)
            std::cout << "client quit ..." << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

/**
 * @description: 该函数交给创建的线程来执行
 * @param {void} *args
 * @return {*}
 */
void *HandlerRequest(void *args)
{
    pthread_detach(pthread_self());
    int sock = *(int *)args;  //拿出传入的套接字
    delete (int*)args;	//释放传入的参数资源(因为我们传入参数的时候是创建了一个int对象的,我们在函数里面拿到对应的值以后,要把int对象的空间释放,避免资源浪费)

    ServiceIO(sock);
    close(sock);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    // tcp server
    // 1. 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0)
    {
        std::cerr << "socket error: " << errno << std::endl;
        return 2;
    }

    // 2. bind
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    // 3. 建立连接
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error" << std::endl;
        return 4;
    }

    signal(SIGCHLD, SIG_IGN); // 在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

    for (;;)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
        if (new_sock < 0)
        // 如果接收链接失败,就continue跳过服务代码,循环重新尝试接收连接
        {
            continue;
        }
        uint16_t cli_port = ntohs(peer.sin_port);      // 从struct sockaddr_in里面获取port并转成主机序列;
        std::string cli_ip = inet_ntoa(peer.sin_addr); // 从struct sockaddr_in获取点分十进制的字符串ip,inet_ntoa函数,传入结构体里面的一个结构体sin_addr;该函数的作用不仅要把4字节的ip地址转换成点分十进制的字符串形式,还要把网络序列转换成主机序列

        std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl; // new_sock对应的是文件接收连接成功后返回的文件描述符

        pthread_t tid;
        int *pram = new int(new_sock);
        pthread_create(&tid, nullptr, HandlerRequest, pram);
    }

    return 0;
}

client.cc

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>


void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    std::string svr_ip = argv[1];
    uint16_t svr_port = (uint16_t)atoi(argv[2]);

    //1. 创建socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }

    struct sockaddr_in server;
    bzero(&server, sizeof(server));	//将缓冲区的空间全部清零
    server.sin_family = AF_INET;

    //1. 将点分十进制的字符串风格的IP,转化成为4字节IP
    //2. 将4字节由主机序列转化成为网络序列
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str()); //server ip,我们的ip是点分十进制的字符串风格的IP,要转化成为4字节IP
    server.sin_port = htons(svr_port); // server port,port是主机序列,是小端,要转成网络序列

    //2. 发起链接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
        std::cout << "connect server failed !" << std::endl;
        return 3;
    }

    std::cout << "connect success!" << std::endl;

    // 进行业务请求
    while(true)
    {
        std::cout << "Please Enter# ";
        char buffer[1024];
        fgets(buffer, sizeof(buffer)-1, stdin);

        write(sock, buffer, strlen(buffer));

        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    return 0;
}

4.运行结果

client

server


  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值