通过线程的形式来实现网络控制

分文件编程实现客户端和服务器的连接
socket.h 
#ifndef __SOCKET__H
#define __SOCKET__H
    
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <errno.h>

#define IPADDR "192.168.31.191" //自己实际的IP地址,通过cmd查询
#define IPPORT "8989" //注意检查自己的端口是否已被占用

//IP地址和端口号通过传参的形式传入
int socket_init(const char *ipaddr,const char *port);

#endif // __SOCKET_H__

socket.c 
#include "socket.h"

int socket_init(const char *ipaddr,const char *port)
{
    int s_fd = -1;
    int  ret = -1;
 
    struct sockaddr_in s_addr;
 
    //每次清空之后再配置
    memset(&s_addr,0,sizeof(struct sockaddr_in));
 
    // socket()  创建一个socket,该socket与本地的 8989 端口绑定,并阻塞式等待接收客户端的连接
    s_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(s_fd == -1)
    {
        perror("socket error");
        exit(1);
    }else{
        printf("socket success\n");
    }
 
    //配置socket  设置本地要绑定的IP地址和端口号,通过传参的形式来设置
    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(atoi(port));
    inet_aton(ipaddr,(struct in_addr *)&s_addr.sin_addr);
 
    // bind()  
    int ret = bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
    if(ret == -1){
        perror("bind error\n");
        return -1;
    }else{
        printf("bind success\n");
    }
 
    // listen()  通知内核可以连接1个客户端
    ret = listen(s_fd,1);
    if(ret == -1){
        perror("listen error\n");
        return -1;
    }else{
        printf("listen success\n");
    }

    return s_fd;
}

main.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <wiringPi.h>
#include <pthread.h>

#include "socket.h"

pthread_cond_t cond;
pthread_mutex_t mutex;

void *pget_socket(void *arg)
{
    int s_fd = -1;
    int c_fd = -1;
    char buffer[6];
    int nread = -1;

    //配置客户端socket
    struct sockaddr_in c_addr;
    int clen = sizeof(struct sockaddr_in);

    //每次清空之后再配置
    memset(&c_addr,0,sizeof(struct sockaddr_in));

    //配置socket  将已经宏定义好的IP地址和端口号传入
    s_fd = socket_init(IPADDR,IPPORT);
    if(s_fd == -1)
    {
        //socket初始化失败,打印调试信息,明确出错点
        printf("socket init failed\n");
        //线程退出
        pthread_exit(0);    
    }
    
    while(1)
    {
        //等待客户端连接
        c_fd = accept(s_fd, (struct sockaddr *)&c_addr, (socklen_t *)&clen);

        //引入TCP KeepAlive功能,检测客户端的异常退出情况
        int keepalive = 1; // 开启TCP KeepAlive功能
        int keepidle = 5; // tcp_keepalive_time 5s内没收到数据开始发送心跳包
        int keepcnt = 3; // tcp_keepalive_probes 每次发送心跳包的时间间隔,单位秒
        int keepintvl = 3; // tcp_keepalive_intvl 每3s发送一次心跳包

        setsockopt(c_fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive,sizeof(keepalive));
        setsockopt(c_fd, SOL_TCP, TCP_KEEPIDLE, (void *) &keepidle, sizeof(keepidle));
        setsockopt(c_fd, SOL_TCP, TCP_KEEPCNT, (void *)&keepcnt, sizeof(keepcnt));
        setsockopt(c_fd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepintvl, sizeof(keepintvl));
        
        printf("%s|%s|%d: Accept a connection from %s:%d\n",__FILE__,__func__,__LINE__, inet_ntoa(c_addr.sin_addr),ntohs(c_addr.sin_port));//打印调试信息+客户端的IP地址和端口号,明确出错点

        if(c_fd == -1)
        {
            //客户端连接失败,打印调试信息,明确出错点
            perror("accept failed\n");
            //继续等待客户端的连接
            continue;
        } 
        while(1)
        {
            memset(buffer,0,sizeof(buffer));
            nread = recv(c_fd,buffer,sizeof(buffer),0); //recv()等同于read()  等待读取对方发来的信息,如果接收不到消息会一直阻塞在这边
            if(nread > 0){
                if(strstr(buffer,"open")){
                        pthread_mutex_lock(&mutex); //拿到互斥锁
                        pthread_cond_signal(&cond); //达到条件,通知等待线程
                        pthread_mutex_unlock(&mutex);   //线程执行完成之后释放锁
                }
            }else{
                break;  //break掉当前while循环,重新去等待连接客户端接入
            }
        }
        close(c_fd);
    }
    pthread_exit(0);
}   

//主线程
int main(){
    pthread_t get_socket_tid;
    wiringPiSetup();    //  初始化wiringPi

    //创建线程处理客户端的请求
    //此处是创建网络控制线程  
    pthread_create(&get_socket_tid, NULL, pget_socket, NULL);

    pthread_join(get_socket_tid, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

线程的相关概念
与进程的区别是什么?

进程——资源分配的最小单位,线程——程序执行的最小单位。

典型的Unix/Linux进程可以看成是一个控制线程,一个进程在同一时刻只能做一件事情。有了多个控制线程之后,可以把进程设计成在同一时刻做多件事情,每个线程各自处理独立的任务。即一个进程中可以并发多个线程,每条线程并行执行不同的任务。

把进程看作一个基本单位时,它是线程的容器,负责分配系统资源,如CPU时间、内存等。把线程看作最小单位时,它可以由操作系统进行运算调度,是进程中的实际运作单位。

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

从进程和线程的区别出发,我们使用线程的原因是:进程有独立的内存空间,线程没有,同一进程内的线程共享进程的地址空间。对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

线程的优势是什么?
  1. 节俭的多任务操作方式。启动线程花费的空间小于进程,线程间彼此切换所花的时间小于进程间切换。

  2. 线程间的通信机制很方便。同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接给其他线程用,方便且快捷。而不同进程都具有独立的数据空间,要想进行数据传递只能通过通信的方式进行,费时且不方便。

多线程需要注意的地方:数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击。

与线程相关的API


#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

int pthread_exit(void *rval_ptr);    //跟pthread_join配合使用,线程执行完毕之后,退出这个线程,然后才继续往下执行后面的程序

int pthread_join(pthread_t thread, void **rval_ptr);    //调用后函数线程将一直阻塞,等待相关的线程执行完毕

int pthread_detach(pthread_t thread);    //把指定的线程转变为脱离状态,让自己脱离线程时使用pthread_detach(pthread_self());

对于多线程程序来说,我们往往需要对这些多线程进行同步。同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。

与互斥锁相关的API


#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);    //初始化锁

int pthread_mutex_destroy(pthread_mutex_t mutex);    //销毁锁

int pthread_mutex_lock(pthread_mutex_t mutex);    //加锁

int pthread_mutex_trylock(pthread_mutex_t mutex);

int pthread_mutex_unlock(pthread_mutex_t mutex);    //释放锁
 

当多个线程都会对同一资源进行操作时,为了防止数据不可控,可以通过使用互斥锁+条件变量来达到我们的要求。

在访问共享资源前,对互斥量进行加锁,在访问完成之后解锁。对互斥量加锁之后,其他想要对互斥量加锁的线程都会阻塞,直到当前线程释放该互斥锁。若释放互斥锁时与其相关的多个线程阻塞,这些都是处于可运行状态的线程,那么第一个变为可运行状态的线程会首先拿到锁。当前只有这一个线程可以向前运行,其他的线程要继续等待,直到下一次加锁成功。

什么情况下造成死锁?

场景:有两把锁1锁和2锁、两个线程 线程1和线程2

两个线程手里各握着一把锁,此时线程1想要拿到对方手里的锁,但是对方不释放锁,而且还想拿到自己手里的锁。此时两个线程手里的锁都被牢牢握住,同时又想要拿到对方的锁,这种情况下是拿不到的,还会造成阻塞,形成死锁。

与条件变量相关的API


#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);    //初始化条件变量

int pthread_cond_destroy(pthread_cond_t cond);    //销毁条件变量

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

//以下两个函数通知线程条件已经满足


int pthread_cond_signal(pthread_cond_t cond);    //唤醒等待该条件的某个线程


int pthread_cond_broadcast(pthread_cond_t cond);    唤醒等待该条件的所有进程
 

为什么要使用条件变量?

多线程同时运行时,谁拿到优先权是不一定的,具有随机性。如果条件变量和互斥量一起使用,可以允许线程以无竞争的方式等待特定条件的发生。

TCP心跳机制解决socket异常断开问题
为什么需要考虑socket客户端断开的情形?


如果强行关掉客户端,server会收到一些奇怪的信息,并异常退出。

当服务器监听并接收一个客户端连接时,可以不断向客户端发送数据,这时如果客户端断开socket连接,服务器继续像一个关闭的socket发送数据,系统会默认对服务器进程发送一个SIGPIPE信号,默认终止当前服务进程。

原因:强行关闭客户终端后,client进程交付给初始进程。等初始进程查询到client后将它杀掉。但是在杀掉之前,由于关掉了终端(主要是关掉了输入缓冲区),导致本来阻塞中的cin或scanf返回EOF,程序得以继续执行send和recv操作。服务器发现client有消息传入,但是在尝试回应client的时候client被初始进程杀掉。然后就变成了给已经关闭的socket发送数据。

客户端断开的情形有哪些?

1. 客户端能够发状态给服务器:正常断开、强制关闭客户端、客户端能够作出反应等
2. 客户端不能发状态给服务器:异常断开如突然断网、断电、客户端卡死,此时客户端没时间作出反应,服务器也不了解客户端的状态,导致服务器进入异常等待。

如何解决客户端异常断开问题?

采用TCP心跳包机制——服务器定时向客户端发送消息并接收其回应,如果客户端有回应就代表连接正常;如果在探测时间和探测次数内没有接收到来自客户端的回应,服务器可以判定为连接已经断开。

心跳包的机制有一种方法就是采用TCP_KEEPALIVE机制,它是一种用于检测TCP连接是否存活的机制,它的原理是在一定时间内没有数据往来时,发送探测包给对方,如果对方没有响应,就认为连接已经断开

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值