Linux教程

Linux&C++:网络编程最原始服务端及客户端代码实现及函数释义

本文主要是介绍Linux&C++:网络编程最原始服务端及客户端代码实现及函数释义,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言:学习编程一定要敲,接着测试,然后查资料,最后总结!!!

socket这个单词在C++网络编程中的含义

socket中文就是插座。运行在计算机中的两个程序通过socket建立起一个通道,数据在通道中传输。
socket把复杂的TCP/IP协议族隐藏了起来,对程序员来说,只要用好socket相关的函数,就可以完成网络通信。

在这里插入图片描述

socket分类

socket有两种通信机制,如下:

流(stream)数据报(datagram)
又称流socket数据报socket
依赖协议基于TCP协议UDP协议
效果是一个有序、可靠、双向字节流的通道,传输数据不会丢失、不会重复、顺序也不会错乱。就像两个人在打电话,接通后就在线了,您一句我一句的聊天。不需要建立和维持连接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但是它的速度比较高。就像短信功能,一个人向另一个人发短信,对方不一定能收到。

:在实际开发中,数据报socket的应用场景极少,本文也只介绍流socket。

客户端和服务端流程简介

在这里插入图片描述

服务端代码实现

这部分是我自己改编的和参考资料的略有不同,但逻辑是相同的。

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <iostream>
using namespace std;


int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        printf("Warning:Parameters must be set!\nPls Using:./server portNumber\nExample:./server 5005\n\n");
        return -1;
    }
    printf("-------------------Server start!\n");

    // 第1步:创建服务端的socket。
    int serverSocket;
    serverSocket = socket(AF_INET, SOCK_STREAM, 0); //AF_INET 协议族,SOCK_STREAM TCPsocket类型:失败返回-1,成功返回该socket的整型数值//#include <sys/socket.h>
    if (serverSocket == -1)
    {
        perror("create socket error:\n"); //perror(s) 就是将发生错误的函数的message(也就是errno中的message)和s的信息一起打印出来。此处就是socket+socket失败后存在全局变量errno中的message
        return -1;
    }
    else
    {
        printf("1.Create Socket Successfully\n");
    }

    //第2步:把服务端用于通信的地址和端口绑定到socket上。
    //2.1 给地址信息结构体赋上传入的值
    //新建结构体指针 并进行内存清0操作
    struct sockaddr_in servaddr;          // 地址信息的数据结构。//#include <arpa/inet.h>
    memset(&servaddr, 0, sizeof(servaddr)); //将第一个参数后的第三个参数大小的内存置为第二个参数的直,就是内存清0常用函数//#include <string.h>
    //协议族赋值
    servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。
    //IP地址赋值 可指定IP或获取本机任意IP
    //比如一个主机三个网卡,要接收所有网卡收取的信息就要绑定三次,但是可以通过I NADDR_ANY 可以直接接收所有的网卡收到的信息
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //本机任意ip地址都能收到。 htonl 将一个32位数从主机字节顺序转换成网络字节顺序。
    //servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); //指定ip地址。
    //端口号赋值
    servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。atoi把字符串转换成整型数的一个函数,会跳过前面的空白字符(例如空格,tab缩进)等//#include <stdlib.h>

    //将通信信息绑定到创建的服务端socket上
    int bindResult;
    bindResult = bind(serverSocket, (struct sockaddr *)&servaddr, sizeof(servaddr)); //成功返回0,失败返回-1,错误存在errno中//获取指针长度用*servaddr
    if (bindResult == -1)
    {
        perror("bind error:\n");
        close(serverSocket); //即使失败也要关闭socket,释放资源 //#include <unistd.h>
        return -1;
    }
    else
    {
        printf("2.Bind successfully!\n");
    }

    // 第3步:把socket设置为监听模式。服务端特有的
    int listenResult;
    listenResult = listen(serverSocket, 5); //0-成功, -1-失败;第二个参数一般不超过30
    if (listenResult == -1)
    {
        perror("listen error:\n");
        close(serverSocket);
        return -1;
    }
    else
    {
        printf("3.Listen successfully!\n");
    }
    // 第4步:接受客户端的连接。
    //新建一个用于存储客户端通信信息的结构体指针
    struct sockaddr_in clientaddr;
    int socklen = sizeof(struct sockaddr_in);  
    int acceptResult;
    acceptResult = accept(serverSocket, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen); //如果客户端没连上会一直处于等待状态,又称阻塞; 第三个参数和bind的第三个参数不同,是用来存放客户端通信信息的
    if (acceptResult == -1)
    {
        printf("Server accept error:\n");
        close(serverSocket);
        return -1;
    }
    else
    {
        printf("4.Client Accept! ip:(%s)\n ---------------Communication Start---------\nWarning:If you input 'bye',this communication will end!!!\n", inet_ntoa(clientaddr.sin_addr));
    }

    char Getmessage[1024];//变量名就代表该数组首地址
    while (1)
    {
        int recvResult;
        memset(Getmessage,0,sizeof(Getmessage));//内存清零操作
        //recv函数对端关闭返回0,出错返回-1
        recvResult=recv(acceptResult,Getmessage,sizeof(Getmessage),0);// 第二个参数用来接收链接上的客户端发送的消息,第三个参数要接收信息的长度,不能超过接收实际信息的长度;第二个参数虽然是指针类型,但数组名等同于char * message;的变量名的意义
        if (recvResult<=0)
        {
            printf("recv error!\n");
            break;
        }
        else
        {
            printf("@@GetMessage:\n%s\n",Getmessage);
            if (((string)Getmessage)=="bye")
            {
                printf("---------------Communication End---------\n");
                break;
            }
            else
            {
                printf("##PlsInputMessage:\n");
                string sendMessage;
                cin>>sendMessage;
                int sendResult;
                与recv函数一样对端关闭返回0,出错返回-1
                sendResult=send(acceptResult,(const char *)(sendMessage.data()),sizeof(sendMessage),0);
                if (sendResult<=0)
                {
                    perror("send error:\n");
                    break;
                }
                else
                {
                    printf("---Send Successfully! Waiting the other party send a message...\n");
                    if ((string)sendMessage=="bye")
                    {
                        printf("---------------Communication End---------\n");
                        break;
                    }
                }
            }
        }
    }
    
    close(serverSocket);
    close(acceptResult);
}

客户端代码实现

#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string>
#include <iostream>
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 3) //运行客户端需要知道IP地址及端口
    {
        printf("Warning:Parameter must be set\n Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n");
        return -1;
    }

    // 第1步:创建客户端的socket。
    int clientSocket;
    clientSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSocket == -1) //socket函数返回值实际是一个文件描述符,是一个整数;失败返回-1
    {
        perror("socket erroe:\n");
        return -1;
    }
    else
    {
        printf("1.Create Socket Successfully!\n");
    }

    // 第2步:向服务器发起连接请求。
    struct hostent *h;
    h = gethostbyname(argv[1]); //gethostbyname将ip地址或域名转化为hostent结构体表达式的地址(指针地址);失败返回NULL
    if (h == NULL)
    {
        printf("gethostbyname failed.\n");
        close(clientSocket);
        return -1;
    }

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr)); //memset:将&servaddr中当前位置后sizeof(servaddr)个字节用0代替。通常为新申请的内存做初始化工作,也是对较大的结构体或数组进行清零操作的一种最快方法。
                                            //第二个参数设置为0是为了将,&servaddr开始的地址内存按sizeof(servaddr)个字节清0
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));           // 指定服务端的通信端口。
    memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); //memcpy:内存拷贝函数。将h->h_addr的h->h_length个字符拷贝到&servaddr.sin_addr中。

    int connectResult;
    connectResult = connect(clientSocket, (struct sockaddr *)&servaddr, sizeof(servaddr)); //将创建的客户端socket连接至服务端,第二个参数为(struct sockaddr *)类型,第三个为第二个参数的大小;成功返回0,失败返回-1
    if (connectResult == -1)
    {
        perror("connect error");
        close(clientSocket);
        return -1;
    }
    else
    {
        printf("2.Connect Successfully! ip:(%s)\n ---------------Communication Start---------------\n Warning:If you input 'bye',this communication will end!!!\n", inet_ntoa(servaddr.sin_addr));
    }

    // char buffer[1024]; //开辟1Kb空间;字符数组,可在栈或堆上分配空间,堆上需手动管理内存。
    // 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
    while (1)
    {
        int sendResult;
        string sendMessage;//使用string时无法输入空格,因为容器string遇到空格会截断数据
        printf("##PlsInputMessage:\n");
        cin >> sendMessage;
        sendResult = send(clientSocket, (const char *)sendMessage.data(), sizeof(sendMessage), 0);
        if (sendResult <= 0)
        {
            perror("send error:\n");
            break;
        }
        else
        {
            if (sendMessage=="bye")
            {
                printf("---------------Communication End---------------\n");
                break;
            }
            else
            {
                printf("---Send Successfully! Waiting the other party send a message...\n");
                string Getmessage;
                int recvResult;
                recvResult = recv(clientSocket, (char *)(Getmessage.c_str()), sizeof(Getmessage), 0);
                if (recvResult <= 0)
                {
                    printf("recv error!\n");
                    break;
                }
                else
                {
                    printf("@@GetMessage:\n%s\n", (char *)Getmessage.c_str());
                    if ((string)Getmessage.c_str()=="bye")
                    {
                        printf("---------------Communication End---------------\n");
                        break;
                    }
                }
            }
        }
    }
}

测试的客户端和服务端程序

自行将上面的代码编译后测试,这里不做赘述。
注意:记得设置端口时,要提前将端口的防火墙打开(此命令参照连接)

服务端函数详解

服务端函数调用的流程是:socket->bind->listen->accept->recv/send->close

1. socket函数

  • 预备知识:
 在UNIX系统中,一切输入输出设备皆文件,socket()函数的返回值其本质是一个文件描述符,是一个无符号整数。

unix系统中默认已有三个文件描述符如下:

0:标准输入
1:标准输出
2:标准错误

故socket程序中使用socket创建的描述符返回值一般是从3开始返回。
但GDB调试时是从7开始的。

- socket函数作用:socket函数用于创建一个新的socket,也就是向系统申请一个socket资源。
- 函数声明int socket(int domain, int type, int protocol);
- 返回值:成功则返回一个socket,失败返回-1,错误原因存于errno 中
- 参数说明
第一个参数(domain)只能填AF_INET;
第二个参数(type)只能填SOCK_STREAM;这个参数能填好多种可自行百度了解。
第三个参数(protocol)只能填0。

domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。

protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

关于此函数可用如下命令在linux系统中查看原文函数释义:

man socket

2. bind函数

- 作用:服务端把用于通信的地址和端口绑定到socket上。
- 函数声明int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

- 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误。
- 参数说明
第一个参数(sockfd)为要绑定的socket对于应的文件描述符的值,也就是socket函数返回的整数;
第二个参数(addr)为结构体sockaddr,一般我们会先定义结构体sockaddr_in(struct sockaddr_in servaddr;)并给其结构体中的成员赋值,然后再强转为sockaddr* 类型((struct sockaddr *)&servaddr)。
第三个参数(addrlen)表示第二个参数addr结构体的大小。一般直接sizeof(servaddr)即可;
3. listen函数
- 作用:listen函数把主动连接socket变为被动连接的socket,使得这个socket可以接受其它socket的连接请求,从而成为一个服务端的socket。
- 函数声明int listen(int sockfd, int backlog);
- 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
- 参数说明
第一个参数(sockfd):为已经被bind过的socket描述符;
socket函数返回的socket是一个主动连接的socket。在服务端的编程中,程序员希望这个socket可以接受外来的连接请求,也就是被动等待客户端来连接。由于系统默认时认为一个socket是主动连接的,所以需要通过某种方式来告诉系统,程序员通过调用listen函数来完成这件事。
第二个参数(backlog):为整型数字;这个参数涉及到一些网络的细节,比较麻烦,填5、10都行,一般不超过30。
注:当调用listen之后的socket就可以再使用accept来接受客户端的连接请求。
4. accept函数
- 作用:服务端接受客户端的连接。
- 函数声明int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
- 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
accept在等待的过程中,如果被中断或其它的原因,函数返回-1,表示失败,如果失败,可以重新accept。
- 参数说明
第一个参数(sockfd):是已经被listen函数调用过的socket文件描述符。
第二个参数(addr):同bind函数的第二个参数;用于存放接收监听到链接上来的客户端的地址信息。如果不需要客户端的地址,可以填0(这个参数是个地址,0也就是NULL)。
第三个参数(addrlen):用于存放第二个参数的长度,如果addr为0,addrlen也填0。
4. recv函数
- 作用:用于接收对端socket发送过来的数据。不论是客户端还是服务端,应用程序都用recv函数接收来自TCP连接的另一端发送过来数据。
- 函数声明ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t 就是 长整型long int。
- 返回值
1.如果socket的对端没有发送数据,recv函数就会等待
2.如果对端发送了数据,函数返回接收到的字符数
3.对端关闭,返回值为0
4.出错时返回-1
- 参数说明
第一个参数(sockfd):为accept函数返回值。
第二个参数(buf):为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。
第三个参数(len):len需要接收数据的长度,不能超过第二个参数(buf)的大小,否则内存溢出。
第四个参数(flags):一般填0, 其他数值意义不大。
5. send函数
- 作用:把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。
- 函数声明ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t 就是 长整型long int。
- 返回值
函数返回已发送的字符数,出错时返回-1,错误信息errno被标记。
就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。
- 参数说明
第一个参数(sockfd):服务端为accept函数返回的socket;客户端为最初创建的socket。
第二个参数(buf):为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。
第三个参数(len):len需要接收数据的长度,不能超过第二个参数(buf)的大小,否则内存溢出。
第四个参数(flags):一般填0, 其他数值意义不大。

总结
服务端总共用到两个文件描述符(socket):
1.用于建立通道并绑定地址端口(bind)和用来监听(listen)以及等待(accept)的最初创建的socket。
2.由accept函数创建的用于与客户端收发报文的socket。

如果send函数和recv返回值(<=0),表示通信链路已不可用。

5. close函数
- 作用:关闭socket
- 函数声明int close(int sockfd);
- 返回值: 成功返回0,出错为-1
- 参数说明:只有一个参数sockfd,就是文件描述符(socket)的值。

客户端函数详解

客户端函数调用的流程是:socket->connect->send/recv->close
1. socket函数
同服务端。
2. gethostbyname函数
- 作用:把ip地址或域名转换为hostent 结构体表达的地址。它还有解析域名的作用。
- 函数声明struct hostent *gethostbyname(const char *name);
- 返回值:如果成功,返回一个hostent结构指针,失败返回NULL。
- 参数说明:name:域名或者主机名,例如"192.168.1.3"、"www.freecplus.net"等。
3. connect函数
- 作用:向服务器发起连接请求。只用于客户端。connect函数用于将参数sockfd 的socket 连至参数serv_addr 指定的服务端,参数addrlen为sockaddr的结构长度。
- 函数声明int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
- 返回值: 成功则返回0,失败返回-1,错误原因存于errno 中。如果服务端的地址错了,或端口错了,或服务端没有启动,connect一定会失败。
- 参数说明
第一个参数(sockfd)为要绑定的socket对于应的文件描述符的值,也就是socket函数返回的无符号整数;
第二个参数(addr)为结构体sockaddr,一般我们会先定义结构体sockaddr_in(struct sockaddr_in servaddr;)并给其结构体中的成员赋值,然后再强转为sockaddr* 类型((struct sockaddr *)&servaddr)。
第三个参数(addrlen)表示第二个参数addr结构体的大小。一般直接sizeof(servaddr)即可;
4. recv函数
同服务端
5. send函数
通服务端
6. close 函数
同服务端

关闭socket

socket是系统资源,操作系统打开的socket数量是有限的,在程序退出之前必须关闭已打开的socket,就像关闭文件指针一样,就像delete已分配的内存一样,极其重要。

补充知识

- 在设置服务端地址时应注意下面这种情况:
服务器可能有多个网卡,如下图示例:
网卡1:地址:192.168.10.10;192.168.10.12和192.168.10.13访问服务器时只能通过该网卡;10.153.10.20和10.153.10.22就不能通过该网卡访问服务器。
网卡2地址:10.153.10.10;10.153.10.20和10.153.10.22访问服务器时只能通过该网卡;192.168.10.12和192.168.10.13就不能通过该网卡访问服务器
在这里插入图片描述

- bind和connect函数的第二个参数sockaddr_in

示例代码如下:
设置地址时需将服务端bind和connect的servaddr.sin_addr.s_addr结构设置一样,比如都采用主机字节序,或都不采用主机字节序。否则会产生错误。一般开发都使用网络字节序。

	struct sockaddr_in servaddr;          // 地址信息的数据结构。//#include <arpa/inet.h>
    memset(&servaddr, 0, sizeof(servaddr)); //将第一个参数后的第三个参数大小的内存置为第二个参数的直,就是内存清0常用函数//#include <string.h>
    //协议族赋值
    servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。
    //IP地址赋值 可指定IP或获取本机任意IP
    //比如一个主机三个网卡,要接收所有网卡收取的信息就要绑定三次,但是可以通过I NADDR_ANY 可以直接接收所有的网卡收到的信息
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //本机任意ip地址都能收到。 htonl 将一个32位数从主机字节顺序转换成网络字节顺序。
    //servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); //指定ip地址。
    //端口号赋值
    servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。atoi把字符串转换成整型数的一个函数,会跳过前面的空白字符(例如空格,tab缩进)等//#include <stdlib.h>

- bind端口绑定
如果一个服务程序(系统中运行的任一程序)已经使用了端口5005,如果你再启动一个服务端程序绑定这个端口,那bind就会报错。

- 打开文件描述符个数问题

//查看能打开的文件操作符个数;一般默认为1024。也就是socket最多能打开1024个
ulimit -n

- 主机字节序与网络字节序
什么是字节序:
在这里插入图片描述网络字节序(大端字节序)和主机字节序(不确定):
在这里插入图片描述

下面是将网络字节序(默认大端)转为小端的转换示例:
转换为二进制后再将二进制按8位进行顺序颠倒后,即得小端字节序的二进制码。
在这里插入图片描述
涉及的字节序转换问题(参考链接):

hotns()——"Host to NetWork Short",主机字节顺序转换为网络字节顺序(对无符号短型进行操作 4bytes)
htonl()——"Host to NetWork Long",主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8bytes)

ntons()——"NetWork to Host short",网络字节序转换为主机字节顺序(对无符号短型进行操作 4bytes)
ntohl()——"NetWork to Host Long",网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8bytes)

- 结构体sockaddr及sockaddr_in
服务端 结构体这块总共涉及三个:sockaddr、sockaddr_in、in_addr.
参考链接
在这里插入图片描述
客户端结构体:在这里插入图片描述
在这里插入图片描述

备注:
参考资料链接

这篇关于Linux&C++:网络编程最原始服务端及客户端代码实现及函数释义的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!