分享自己平时使用的socket多客户端通信的代码技术点和软件使用
分享自己平时使用的socket多客户端通信的代码技术点和软件使用
前言
说到linux下多进程通信,有好几种,之前也在喵哥的公众号回复过,这里再拿出来,重新写一遍:多进程通信有管道,而管道分为匿名和命名管道 ,后者比前者优势在于可以进行无亲缘进程通信;此外信号也是进程通信的一种,比如我们最常用的就是设置ctrl+c的kill信号发送给进程;其次信号量一般来说是一种同步机制但是也可以认为是通信,需要注意的是信号量、共享内存、消息队列在使用时候也有posix和system v的区别;还有我们今天的主角套接字( socket ) :套接字也是一种进程间通信机制。
线程间的通信的话,共享变量,此外在unpipc书描述的话,同步也属于通讯机制,那么就要补充上线程中我们最多用的互斥量、条件变量、读写锁、记录锁和线程中的信号量使用。
今天想分享一些socket编程的例子,socket嵌入式。linux开发很常用,用于进程间通信很方便,也有很多介绍,今天我也也来做自己的介绍分享。和别人不一样的地方,我主要想分享socket 服务端在linux写的代码,使用vscode调试执行,并且同时分享自己使用tcp监控软件去判断socket通信正确性。
作者:良知犹存
转载授权以及围观:欢迎关注微信公众号:羽林君
或者添加作者个人微信:become_me
socket通信基本函数介绍
在这里有一个简单demo演示以及函数的介绍,大家打开这个链接就可以看到哈:
socket重要函数
socket通信有些固定的函数,这里先给大家做简单的分享:
- int socket(int domain, int type, int protocol);
该函数用于创建一个socket描述符,它唯一标识一个socket,这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
1.domain:参数domain表示该套接字使用的协议族,在Linux系统中支持多种协议族,对于TCP/IP协议来说,选择AF_INET就足以,当然如果你的IP协议的版本支持IPv6,那么可以选择AF_INET6,可选的协议族具体见:
- AF_UNIX, AF_LOCAL: 本地通信-AF_INET : IPv4
- AF_INET6 : IPv6
- AF_IPX : IPX - Novell 协议
- AF_NETLINK : 内核用户界面设备
- AF_X25 : ITU-T X.25 / ISO-8208 协议
- AF_AX25 : 业余无线电 AX.25 协议
- AF_ATMPVC : 访问原始ATM PVC
- AF_APPLETALK : AppleTalk
- AF_PACKET : 底层数据包接口
- AF_ALG : 内核加密API的AF_ALG接口
2.type:参数type指定了套接字使用的服务类型,可能的类型有以下几种:
- SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的Socket服务,多用于资料(如文件)传输,如TCP协议。
- SOCK_DGRAM:是提供无保障的面向消息的Socket 服务,主要用于在网络上发广播信息,如UDP协议,提供无连接不可靠的数据报交付服务。
- SOCK_SEQPACKET:为固定最大长度的数据报提供有序的,可靠的,基于双向连接的数据传输路径。
- SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。
- SOCK_RDM:提供不保证排序的可靠数据报层。
3.protocol:参数protocol指定了套接字使用的协议,在IPv4中,只有TCP协议提供SOCK_STREAM这种可靠的服务,只有UDP协议提供SOCK_DGRAM服务,对于这两种协议,protocol的值均为0,因为当protocol为0时,会自动选择type类型对应的默认协议。
- int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
在进行网络通信的时候,必须把一个套接字与一个IP地址或端口号相关联,这个bind就是绑定的过程。
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这个connect()函数用于客户端中,将sockfd与远端IP地址、端口号进行绑定,在TCP客户端中调用这个函数将发生握手过程(会发送一个TCP连接请求),并最终建立一个TCP连接,而对于UDP协议来说,调用这个函数只是在sockfd中记录远端IP地址与端口号,而不发送任何数据,参数信息与bind()函数是一样的。
- int listen(int s, int backlog);
listen()函数只能在TCP服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函数在一般在bind()函数之后调用,在accept()函数之前调用,它的函数原型是:
- int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
accept()函数就是用于处理连接请求的,accept()函数用于TCP服务器中,等待着远端主机的连接请求,并且建立一个新的TCP连接,在调用这个函数之前需要通过调用listen()函数让服务器进入监听状态,如果队列中没有未完成连接套接字,并且套接字没有标记为非阻塞模式,accept()函数的调用会阻塞应用程序直至与远程主机建立TCP连接;如果一个套接字被标记为非阻塞式而队列中没有未完成连接套接字, 调用accept()函数将立即返回EAGAIN。
- ssize_t read(int fd, void *buf, size_t count);
read 从描述符 fd 中读取 count 字节的数据并放入从 buf 开始的缓冲区中.
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
不论是客户还是服务器应用程序都可以用recv()函数从TCP连接的另一端接收数据,它与read()函数的功能是差不多的。
- ssize_t write(int fd, const void *buf, size_t count);
write()函数一般用于处于稳定的TCP连接中传输数据,当然也能用于UDP协议中,它向套接字描述符 fd 中写入 count 字节的数据,数据起始地址由 buf 指定,函数调用成功返回写的字节数,失败返回-1,并设置errno变量。
- int send(int s, const void *msg, size_t len, int flags);
无论是客户端还是服务器应用程序都可以用send()函数来向TCP连接的另一端发送数据。
- int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
sendto()函数与send函数非常像,但是它会通过 struct sockaddr 指向的 to 结构体指定要发送给哪个远端主机,在to参数中需要指定远端主机的IP地址、端口号等,而tolen参数则是指定to 结构体的字节长度。
- int close(int fd);
close()函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符
TCP客户端一般流程
- 调用socket()函数创建一个套接字描述符。
- 调用connect()函数连接到指定服务器中,端口号为服务器监听的端口号。
- 调用write()函数发送数据。
- 调用close()函数终止连接。
 // 创建套接字描述符
((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
// 建立TCP连接
(connect(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr))
write(sockfd, buffer, sizeof(buffer))
close(sockfd);
TCP服务器一般流程
- 服务器的代码流程如下:
- 调用socket()函数创建一个套接字描述符。
- 调用bind()函数绑定监听的端口号。
- 调用listen()函数让服务器进入监听状态。
- 调用accept()函数处理来自客户端的连接请求。
- 调用read()函数接收客户端发送的数据。
- 调用close()函数终止连接。
// socket create and verification
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// binding newly created socket to given IP and verification   
if ((bind(sockfd, (struct sockaddr*)&server, sizeof(server))) != 0) 
// now server is ready to listen and verification
if ((listen(sockfd, 5)) != 0) {
// accept the data packet from client and verification
connfd = accept(sockfd, (struct sockaddr*)&client, &len);
if (read(connfd, buff, sizeof(buff)) <= 0) {
close(connfd);
close(sockfd);
这里也顺带分享一个socket 阻塞和非阻塞的机制 前面提到accept函数中,描述套接字没有标记为非阻塞模式,accept()函数的调用会阻塞应用程序直至与远程主机建立TCP连接;如果一个套接字被标记为非阻塞式而队列中没有未完成连接套接字, 调用accept()函数将立即返回EAGAIN。但是socket默认初始化是阻塞的,正常初始化后accept没有收到客户端的链接请求的话,就会一直的等待阻塞当前线程,直到有客户端进行链接请求。
那么如何才能把socket设置为非阻塞呢?用ioctl(sockfd, FIONBIO, &mode);
//-------------------------
// Set the socket I/O mode: In this case FIONBIO
// enables or disables the blocking mode for the
// socket based on the numerical value of iMode.
// If iMode = 0, blocking is enabled;
// If iMode != 0, non-blocking mode is enabled.
u_long iMode = 1;  //non-blocking mode is enabled.
ioctlsocket(m_socket, FIONBIO, &iMode); //设置为非阻塞模式
一般大家介绍会说使用ioctlsocket,但是有些系统使用会报错。如下:
ioctlsocket会报错,所以使用 ioctl就好了,操作都是一样的。
 #include <sys/ioctl.h>
ioctl(sockfd, FIONBIO, &mode);
 
这是一个简单的图表分析,来自下面文章链接,大家有兴趣也可以自行查看。
 
阻塞非阻塞的介绍 链接:
代码实例
代码有test_socket_client.cpp 、test_socket_server.h、test_socket_server.cpp 三个文件,交互机制以及实现功能如下:
首先test_socket_client.cpp 是客户端代码,用来测试链接服务器端交互,用select进行接收数据,并监听执行终端是否有输入信息,输入信息立刻发送。
test_socket_server.h是test_socket_server.cpp使用定义的类和api的头文件,而在test_socket_server.cpp实现了定义了一个支持多客户端连接的通信接口,同时也时刻检测执行终端输入信息,并广播到全部链接的客户端;而客户端发过来的信息,服务端针对的点对点收发,即接收到特定客户端的信息只发送到该客户端。其中使用了std::future + std::async实现了通信的异步操作,并使用 impl模式包裹了socket接口。在监听执行终端信息时候分别使用了std::condition和std::async实现,大家可以通过宏开关自行选择测试。
还有些其他的技术使用,多线程的调度以及流的输出,忽略SIGPIPE信号用来控制客户端链接断开之后代码正常运行等,再后面我一一给大家分析介绍。
test_socket_client.cpp 这个文件就是随便找了一个socket客户端代码,这个test_socket_client代码来源是网络,大家也可以自己去写或者网上自己找相关的用例,因为本次的重要部分是服务端server代码,所以这块就贴一下代码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
//g++ test_socket_client.cpp -o  test_socket_client
#define BUFLEN 1024
#define PORT 8555
int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in s_addr;
    socklen_t len;
    unsigned int port;
    char buf[BUFLEN];
    fd_set rfds;
    struct timeval tv;
    int retval, maxfd; 
    
    /*建立socket*/
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        perror("socket");
        exit(errno);
    }else
        printf("socket create success!\n");
    /*设置服务器ip*/
    memset(&s_addr,0,sizeof(s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(PORT);
    if (inet_aton("127.0.0.1", (struct in_addr *)&s_addr.sin_addr.s_addr) == 0) {
        perror("127.0.0.1");
        exit(errno);
    }
  
    /*开始连接服务器*/ 
    while(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){
        perror("connect");
        sleep(1);
        exit(errno);
    }
    while(1){
        FD_ZERO(&rfds);
        FD_SET(0, &rfds);
        maxfd = 0;
        FD_SET(sockfd, &rfds);
        if(maxfd < sockfd)
            maxfd = sockfd;
        tv.tv_sec = 6;
        tv.tv_usec = 0;
        retval = select(maxfd+1, &rfds, NULL, NULL, &tv);
        if(retval == -1){
            printf("select出错,客户端程序退出\n");
            break;
        }else if(retval == 0){
            printf("waiting...\n");
            continue;
        }else{
            /*服务器发来了消息*/
            if(FD_ISSET(sockfd,&rfds)){
                /******接收消息*******/
                bzero(buf,BUFLEN);
                len = recv(sockfd,buf,BUFLEN,0);
                if(len > 0)
                    printf("服务器发来的消息是:%s\n",buf);
                else{
                    if(len < 0 )
                        printf("接受消息失败!\n");
                    else
                        printf("服务器退出了,聊天终止!\n");
                break; 
                }
            }
            /*用户输入信息了,开始处理信息并发送*/
            if(FD_ISSET(0, &rfds)){ 
                /******发送消息*******/ 
                bzero(buf,BUFLEN);
                fgets(buf,BUFLEN,stdin);
               
                if(!strncasecmp(buf,"quit",4)){
                    printf("client 请求终止聊天!\n");
                    break;
                }
                    len = send(sockfd,buf,strlen(buf),0);
                if(len > 0)
                    printf("\t消息发送成功:%s\n",buf); 
                else{
                    printf("消息发送失败!\n");
                    break; 
                } 
            }
        }
    
    }
    /*关闭连接*/
    close(sockfd);
    return 0;
}
test_socket_server.h 使用的头文件,定义一些外部api
#ifndef _TEST_SOCKET_H
#define _TEST_SOCKET_H
#include <functional>
#include <memory>
#include <thread>
#include <vector>
namespace linx_socket {
int Writen(int fd, const void *vptr, int n);
int Readn(int fd, void *vptr, int maxlen);
int CreatSocket(const char *ip, int port);
int StartLisen(int fd);
bool Close(int fd);
}  // namespace linx_socket
class DevSocket  {
 public:
  using CallBack  = std::function<void(int ,std::vector<uint8_t>&&)>;
  DevSocket();
  DevSocket(const CallBack& callback);
  bool Send(int fd,const std::vector<uint8_t> &data) const ;
  // std::vector<uint8_t> Recive() const ; //当建立连接后 就会在线程里面循环读取客户端发来的信息, 所以不需要专门写rx函数
  ~DevSocket(){}
  private:
  class Socket;
  std::unique_ptr<Socket> SocketImpl;
};
#endif
test_socket_server.cpp
里面包含的#include "log.h"这个文件是我自己写的log输出文件,打印时间和颜色等,看着比较方便,大家使用代码时候自行替换成自己需要printf或者std::cout或者自己的打印文件
#include <stdio.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <boost/thread/mutex.hpp>
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <iterator>
#include <string>
#include <thread>
#include <vector>
#include <arpa/inet.h>
#include <errno.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <unistd.h>
#include <future>
#include "test_socket_server.h"
#include "log.h"
// g++ test_socket_server_optimiza_2.cpp -o  test_socket_server_optimiza -lboost_thread -lpthread
namespace linx_socket
{
    constexpr int socket_que_size = 3;
    //使用select进行写入
    int Writen(int fd, const void *vptr, int n)
    {
        ssize_t nleft = n;
        const char *ptr = (const char *)vptr;
        fd_set write_fd_set;
        struct timeval timeout;
        while (nleft > 0)
        {
            ssize_t nwriten = 0;
            timeout.tv_sec = 1;
            timeout.tv_usec = 0;
            FD_ZERO(&write_fd_set);
            FD_SET(fd, &write_fd_set);
            int s_ret = select(FD_SETSIZE, NULL, &write_fd_set, NULL, &timeout);
            if (s_ret < 0)
            {
                EXC_ERROR("-------write_fd_set error------------");
                return -1;
            }
            else if (s_ret == 0)
            {
                usleep(100 * 1000);
                EXC_ERROR("-------write_fd_set timeout ------------");
                continue;
            }
            if ((nwriten = write(fd, ptr, nleft)) < 0)
            {
                if (nwriten < 0 && errno == EINTR)
                {
                    nwriten = 0;
                }
                else
                {
                    EXC_ERROR("-------nwriten error = %d ------------", nwriten);
                    return -1;
                }
            }
            nleft -= nwriten;
            ptr += nwriten;
        }
        return n;
    }
    //使用select进行读取
    int Readn(int fd, void *vptr, int maxlen)
    {
        bool ret = false;
        ssize_t nread = 0;
        fd_set read_fd_set;
        struct timeval timeout;
        while (!ret)
        {
            // EXC_INFO("Readn begine.");
            timeout.tv_sec = 1;
            timeout.tv_usec = 0;
            FD_ZERO(&read_fd_set);
            FD_SET(fd, &read_fd_set);
            int s_ret = select(FD_SETSIZE, &read_fd_set, NULL, NULL, &timeout);
            if (s_ret < 0)
            {
                EXC_ERROR("-------select error------------");
                return -1;
            }
            else if (s_ret == 0)
            {
                usleep(100 * 1000);
                // EXC_ERROR("-------select timeout ------------");
                continue;
            }
            if ((nread = read(fd, vptr, maxlen)) < 0)
            {
                if (errno == EINTR)
                {
                    EXC_ERROR("buff = %d, fd=%d, errno=%d.", vptr, fd, errno);
                    nread = 0;
                }
                else
                {
                    EXC_ERROR("buff = %d, fd=%d, errno=%d.", vptr, fd, errno);
                    return -1;
                }
            }
            else
            {
                if (nread == 0)
                {
                    EXC_ERROR("buff = %d, fd=%d, nread=%d. data:%s", vptr, fd, nread, vptr);
                }
                // else
                // {
                //     EXC_INFO("buff = %d, fd=%d, nread=%d. data:%s", vptr, fd, nread, vptr);
                // }
                ret = 1;
            }
        }
        return nread;
    }
    //进行处理来自客户端的连接请求
    int IsListened(int fd)
    {
        struct sockaddr_in c_addr;
        socklen_t c_lent = sizeof(c_addr);
        int fd_c = accept(fd, (struct sockaddr *)&c_addr, &c_lent);
        if (fd_c < 0)
        {
            if (errno == EPROTO || errno == ECONNABORTED)
            {
                return -1;
            }
        }
        EXC_INFO("accept %s: %d sucess", inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port));
        return fd_c;
    }
    //创建一个套接字描述符
    int CreatSocket(const char *ip, int port)
    {
        int ret = -1;
        // EXC_INFO("CreatSocket");
        int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (fd < 0)
        {
            return -1;
        }
        int reuse = 1;
        //设置套接字的一些选项 SOL_SOCKET:表示在Socket层 SO_REUSEADDR(允许重用本地地址和端口)
        if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
        {
            return -1;
        }
        struct sockaddr_in s_addr;
        memset(&s_addr, 0, sizeof(s_addr));
        s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        s_addr.sin_port = htons(port);
        s_addr.sin_family = AF_INET;
         
                    
                     
                    
                