TCP_Socket 通信2
出错处理函数封装
原理:原函数和包裹函数的函数名差异只有首字母大写,这是因为man page对字母大小写不敏感,同名的包裹函数一样可以跳转至man page , 新包裹需要检查返回值的函数,让代码不那么肥胖。
wrap.h
#ifndef _WRAP_H
#define _WRAP_H
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
void sys_error(const char* str);
int Socket(int domain, int type, int protocol);
int Bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int Listen(int sockfd, int backlog);
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t Readline(int fd, char *buf, ssize_t maxlen);
int tcp4bind(short port, const char *IP);
void print_clientInfo(struct sockaddr_in cliaddr);
ssize_t read_last_line(int fd, char *buf, ssize_t maxlen);
#endif // !_WRAP_H
wrap.c
#include "wrap.h"
void sys_error(const char *str){
perror(str);
exit(1);
}
int Socket(int domain, int type, int protocol){
int n = socket(domain,type,protocol);
if(n < 0){
sys_error("socket error");
}
return n;
}
int Bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen){
int n = bind(sockfd, addr, addrlen);
if (n < 0 )
{
sys_error("bind error");
}
return n;
}
int Listen(int sockfd, int backlog){
int n = listen(sockfd, backlog);
if (n < 0)
{
sys_error("listen error");
}
return n;
}
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen){
int n;
again:
n = accept(sockfd,addr,addrlen);
if(n < 0){
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
sys_error("accept error");
}
return n;
}
int Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen){
int n = connect(sockfd,addr,addrlen);
if(n < 0){
perror("connect error");
}
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes){
int n;
again:
n = read(fd, ptr,nbytes);
if (n == -1)
{
if ((errno == EINTR))
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes){
int n;
again:
n = write(fd, ptr, nbytes);
if (n == -1)
{
if ((errno == EINTR))
goto again;
else
return -1;
}
return n;
}
int Close(int fd){
int n = close(fd);
if (n == -1)
{
sys_error("close error");
}
return n;
}
ssize_t Readn(int fd, void *vptr, size_t n){
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ((nread = read(fd, ptr, nleft)) < 0)
{
if (errno == EINTR)
nread = 0;
else
return -1;
}
else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n){
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ((nwritten = write(fd, ptr, nleft)) <= 0)
{
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
ssize_t my_read(int fd, char *ptr){
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0)
{
again:
if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0)
{
if (errno == EINTR)
goto again;
return -1;
}
else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, ssize_t maxlen){
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++)
{
if ((rc = my_read(fd, &c)) == 1)
{
*ptr++ = c;
if (c == '\n')
break;
}
else if (rc == 0)
{
*ptr = 0;
return n - 1;
}
else
return -1;
}
*ptr = 0;
return n;
}
int tcp4bind(short port, const char *IP){
struct sockaddr_in saddr;
int lfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建socket
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
if(IP == NULL){
saddr.sin_addr.s_addr = htonl(INADDR_ANY); // 系统随机使用可用的ip地址
}else{
if(inet_pton(AF_INET,IP,&saddr.sin_addr.s_addr) <= 0){
perror(IP);
exit(1);
}
}
// 设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
Bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr)); // 绑定服务器地址结构
return lfd;
}
/*打印客户端信息*/
void print_clientInfo(struct sockaddr_in cliaddr)
{
char IP[14] = "";
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, IP, sizeof(IP)),
ntohs(cliaddr.sin_port)); // 打印客户端信息(IP/PORT)
}
/* 读取实际有内容的最后一行 */
ssize_t read_last_line(int fd, char *buf, ssize_t maxlen){
if(fd < 0) return -1;
if( lseek(fd, 0, SEEK_END) == 0 ) { // 移动指针到文件末尾
printf("空文件\n");
return -1;
}
lseek(fd, -1, SEEK_CUR);// 文件指针向前移动一个字符
char tmp = '\0';
while(lseek(fd, -1, SEEK_CUR) > 0){ // 循环到文件开头
read(fd, &tmp, sizeof(char));
if(tmp == '\n') break;
else lseek(fd, -1, SEEK_CUR); // 直到偏移到\n
}
ssize_t offset;
while((offset = Readline(fd, buf, maxlen)) != -1){ // 使用读取一行函数 读取最后一行数据
if(offset > 0) return offset;
else if( offset == 0 ){
lseek(fd, -2, SEEK_CUR); // 向前面移动指针2个字符
tmp = '\0';
while(lseek(fd, -1, SEEK_CUR) > 0){
read(fd, &tmp, sizeof(char));
if(tmp == '\n') break;
else lseek(fd, -1, SEEK_CUR);
}
continue;
}
}
return 1;
}
多进程并发服务器
流程
创建套接字
绑定
监听
while(1){
提取连接
fork();创建子进程
子进程中关闭lfd,服务客户端
父进程中关闭cfd,回收子进程的资源。
}
关闭
代码实现
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include "wrap.h"
#include <signal.h>
#include <sys/wait.h>
// 用于回收子进程
void catch_child(int signum){
while((waitpid(0, NULL, WNOHANG))>0);
return;
}
int main(int argc,char* argv[]){
// 阻塞 (解决信号捕捉还没有注册完,子进程就已经执行完毕了)
sigset_t set;//创建的信号集
sigemptyset(&set);//清空信号集
sigaddset(&set, SIGCHLD);//添加各种信号
// 设置信号屏蔽字
int ret1 = sigprocmask(SIG_BLOCK,&set, NULL);//将自我创建的信号集进行阻塞。
if(ret1 == -1){
sys_error("sigprocmask error");
}
// 创建套接字 、绑定
int lfd = tcp4bind(8000, NULL);
// 监听
Listen(lfd,128);
//提取
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
while(1){
int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 使用inet_ntop将网络字节序转为ip地址字符串
char client_IP[50] = "";
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,
client_IP, sizeof(client_IP)),
ntohs(cliaddr.sin_port)); // 根据accept传出参数,获取客户端 ip 和 port
pid_t pid = fork();
if(pid < 0){
perror("fork error");
exit(0);
}else if(pid == 0){
while(1){
// 关闭lfd
close(lfd);
char buf[1024] = "";
int n = read(cfd,buf,sizeof(buf));
if(n < 0){
perror("read error");
close(cfd);
exit(1);
}else if(n == 0)//客户端关闭
{
printf("客户端关闭连接");
close(cfd);
exit(1);
}else{
write(STDOUT_FILENO, buf, n); // 写到屏幕查看
write(cfd,buf,n);
}
}
}else{// 父进程
close(cfd);
// 回收
struct sigaction act;
act.sa_handler = catch_child;
sigemptyset(&(act.sa_mask));
act.sa_flags = 0; // 默认一般sa_flags=0,本信号屏蔽。
int ret = sigaction(SIGCHLD, &act,NULL);
if(ret != 0){
perror("sigaction error");
exit(1);
}
// 解除阻塞
ret = sigprocmask(SIG_UNBLOCK,&set, NULL);//将自我创建的信号集进行阻塞。
if(ret == -1){
sys_error("sigprocmask error");
}
}
}
return 0;
}
多线程并发服务器
流程
创建套接字
绑定
监听
while(1){
提取连接
pthread_create();// 创建子线程
pthread_detach();// 线程分离,防止出现僵尸线程
}
子进程
void* tfn(void* arg){
// 不能关闭cfd文件描述符
read(cfd);
write(cfd);
pthread_exit(void*(10))
}
代码实现
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdio.h>
#include "wrap.h"
#define PORT 8000
#define IP NULL
// 定义一个数据结构体,用于传输数据给子线程
struct ServerInfo{
int cfd;
struct sockaddr_in caddr;
};
void *pthread_child(void* arg){
struct ServerInfo *s_info = (struct ServerInfo *)arg;
char buf[256] = "";
char str[16] = "";
while(1){
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &(*s_info).caddr.sin_addr, str, sizeof(str)),
ntohs((*s_info).caddr.sin_port)); // 打印客户端信息(IP/PORT)
int n = Read(s_info->cfd,buf,sizeof(buf));
if(n==0){
printf("client closed \n");
break; // 跳出循环
}
printf("received %s \n", buf); // 打印到屏幕上
Write(s_info->cfd,buf,n);
}
close(s_info->cfd);
return (void*)0;
}
int main(int argc, char *argv[])
{
pthread_t tid;
int * a;
pthread_attr_t attr; // 用于设置线程分离
pthread_attr_init(&attr); // 初始化attr结构体
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置分离属性
struct ServerInfo s_info[256]; // 不能不使用结构体数组,不然的话,可能会出现覆盖
int i = 0;
int lfd = tcp4bind(PORT,IP); // 产生套接字和绑定
Listen(lfd, 128); // 设置监听上限
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
printf("server connect start ----- \n");
while(1){
// 提取
int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len);
s_info[i].caddr = cliaddr;
s_info[i].cfd = cfd;
// 创建带有线程分离属性的线程
pthread_create(&tid, &attr, pthread_child, (void *)&s_info[i]);
pthread_attr_destroy(&attr); // 销毁attr属性结构体
// pthread_detach(tid); // 线程分离,防止出现僵尸进程
pthread_join(tid, (void **)&a);
i++;
}
return 0;
}
基础知识
TCP状态图
这里客户端中两个TIME_WAIT
之间的时间是2MSL
半关闭
客户端和服务端都由读和写缓冲区。
当客户端处于FIN_WAIT_2的阶段,那么客户端的写缓冲区就是关闭的状态。
此时客户端不能写,但是还可以收取服务端发送来的数据。
也就是当我们服务端read收取到的是0的时候,那么客户端关闭,
此时处于半关闭的状态。
我们可以通过API实现半关闭状态
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how:
SHUT_RD(0):
关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1):
关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2):
关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
ERRORS
EBADF sockfd is not a valid descriptor.
EINVAL An invalid value was specified in how (but see BUGS).
ENOTCONN
The specified socket is not connected.
ENOTSOCK
The file descriptor sockfd does not refer to a socket.
shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个
心跳包
为了实时检测查询的链接状态,常用的方法就是加入心跳机制。
在接收和发送数据时个人设计一个守护进程(线程),定时发送Heart-Beat包,
客户端/服务器收到该小包后,立刻返回相应的包即可检测对方是否实时在线。
设置TCP属性
设置TCP属性
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
sockfd:套接字
level:选项所在的协议层
SOL_SOCKET 套接字层次
IPPROTO_TCP TCP层次
IPPROTO_IP IP层次
optname:选项名。level对应的选项,一个level对应多个选项,不同选项对应不同功能.
optval:指向某个变量的指针,该变量是要设置新值的缓冲区。
optlen:optval的长度
选项参数optname
level | optname | 说明 | 数据类型 |
---|---|---|---|
SOL_SOCKET: | |||
SO_BROADCAST | 允许发送广播数据报 | int | |
SO_DEBUG | 使能调试跟踪 | int | |
SO_DONTROUTE | 旁路路由表查找 | int | |
SO_ERROR | 获取待处理错误并消除 | int | |
SO_KEEPALIVE | 周期的测试连接是否存活 | int | |
SO_LINGER | 若数据待发送则延迟关闭 | linger{} | |
SO_OOBINLINE | 让接收到的带外数据继续在线存放 | int | |
SO_RCVBUF | 接收缓冲区大小 | int | |
SO_SNDBUF | 发送缓冲区大小 | int | |
SO_RCVLOWAT | 接收缓冲区低潮限度 | int | |
SO_SNDLOWAT | 发送缓冲区低潮限度 | int | |
SO_RCVTIMEO | 接收超时 | timeval{} | |
SO_SNDTIMEO | 发送超时 | timeval{} | |
SO_REUSEADDR | 允许重用本地地址 | int | |
SO_REUSEPORT | 允许重用本地端口 | int | |
SO_TYPE | 取得套接口类型 | int | |
SO_USELOOPBACK | 路由套接口取得发送数据的拷贝 | int | |
IPPROTO_TCP: | |||
TCP_KEEPALIVE | 控测对方是否存活前连接闲置秒数 | int | |
TCP_MAXRT | TCP最大重传时间 | int | |
TCP_MAXSEG | TCP最大分节时间 | int | |
TCP_NODELAY | 禁止Naglesuanfa | int | |
TCP_STDURG | 紧急指针的解释 | int | |
IPPROTO_IP: | |||
IP_HDRINCL | IP头部包括数据 | int | |
IP_OPTIONS | IP头部选项 | int | |
IP_RECVDSTADDR | 返回目的IP地址 | int | |
IP_RECVIF | 返回接收到的接口索引 | int | |
IP_TOS | 服务类型和优先权 | int | |
IP_TTL | 存活时间 | int | |
IP_MULTICAST_IF | 指定外出接口 | in_addr{} | |
IP_MULTICAST_TTL | 指定外出TTL | u_char | |
IP_MULTICAST_LOOP | 指定是否回馈 | u_char | |
IP_ADD_MEMBERSHIP | 加入多播组 | ip_mreq{} | |
IP_DROP_MEMBERSHIP | 离开多播组 | ip_mreq{} |
代码
// 设置每两小时检测心跳,如果对方异常断开,连续多次不同,便会断开连接。
keepAlive = 1;
setsockopt(listenfd,SOL_SOCKET,SO_KEEPALIVE,
(void*)&keepAlive,sizeof(keepAlive)); // 创建完套接字就进行设置。
// 如果需要自己的要求,每几分钟检测心跳,就要自己在应用层写一个报文作为心跳包。
// 心跳包不建议太长。也有例外,乒乓包
// 改变接收缓冲区的大小
int nRecvBUf = 32 * 1024;
setsockopt(listenfd,SOL_SOCKET,SO_RCVBUF,
(void*)&nRecvBUf,sizeof(nRecvBUf)); // 设置接收缓冲区的大小
// 改变发送缓冲区的大小
int nSendBuf = 32 * 1024;
setsockopt(listenfd,SOL_SOCKET,SO_SNDBUF,
(void*)&nSendBuf,sizeof(nSendBuf)); // 设置发送缓冲区的大小