分文件编程实现客户端和服务器的连接
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时间、内存等。把线程看作最小单位时,它可以由操作系统进行运算调度,是进程中的实际运作单位。
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
从进程和线程的区别出发,我们使用线程的原因是:进程有独立的内存空间,线程没有,同一进程内的线程共享进程的地址空间。对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
线程的优势是什么?
-
节俭的多任务操作方式。启动线程花费的空间小于进程,线程间彼此切换所花的时间小于进程间切换。
-
线程间的通信机制很方便。同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接给其他线程用,方便且快捷。而不同进程都具有独立的数据空间,要想进行数据传递只能通过通信的方式进行,费时且不方便。
多线程需要注意的地方:数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为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连接是否存活的机制,它的原理是在一定时间内没有数据往来时,发送探测包给对方,如果对方没有响应,就认为连接已经断开。