linux网络编程socket通信2

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

leveloptname说明数据类型
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_MAXRTTCP最大重传时间int
TCP_MAXSEGTCP最大分节时间int
TCP_NODELAY禁止Naglesuanfaint
TCP_STDURG紧急指针的解释int
IPPROTO_IP:
IP_HDRINCLIP头部包括数据int
IP_OPTIONSIP头部选项int
IP_RECVDSTADDR返回目的IP地址int
IP_RECVIF返回接收到的接口索引int
IP_TOS服务类型和优先权int
IP_TTL存活时间int
IP_MULTICAST_IF指定外出接口in_addr{}
IP_MULTICAST_TTL指定外出TTLu_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));  // 设置发送缓冲区的大小
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值