linux网络编程之简单的服务器多线程

服务器&客户端demo

最近两天了linux的网络编程,自己写了一个demo,熟悉socket的几个主要API的用法:

虚拟机下的ubuntu 14.04 OS,程序包括:

客户端:发送连接请求;一旦连接建立,将用户输入的信息发送给服务器;输入end,客户端程序退出

服务器端:建立监听端口;接收连接请求;为每个请求创建一个线程,接收客户端的信息并打印。


服务器端程序:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>

#define DEBUG
#ifdef DEBUG
#define debug(x) printf(x)
#else
#define debug(x)
#endif

#define SERVER_PORT 1500
#define SERVER_IP "192.168.150.129"
#define LISTEN_NUM 10

typedef void (*sighandler_t)(int);

void sigint_handle(int sig);   //sigint信号处理函数,Ctrl + c所发送的信号
void *func(void *arg);  //线程处理函数

static int fd = -1;

int main(int argc , char * argv[])
{
        int bind_ret = -1;
        struct sockaddr_in sa_in = {0};
        int listen_ret = -1;
        int *accept_ret;
        struct sockaddr_in client_sa = {0};
        socklen_t client_len = 0;

        pthread_t thread = -1;
        int ret = -1;

        sighandler_t sig_ret = (sighandler_t)-1;
        sig_ret = signal(SIGINT,sigint_handle); //设置sigint信号的处理函数
        if(SIG_ERR == sig_ret)
        {
                perror("signal");
                exit(-1);
        }

        fd = socket(AF_INET,SOCK_STREAM,0);  //建立一个监听socket,明确了socket的类型和类别:ipv4和tcp连接
        if(fd < 0)
        {
                perror("soket");
                exit(-1);
        }
        printf("fd:%d\n",fd);

        sa_in.sin_family = AF_INET;
        sa_in.sin_port = htons(SERVER_PORT); //将端口转为网络序,赋给结构体
        sa_in.sin_addr.s_addr = inet_addr(SERVER_IP);  //将点分十进制ip地址转换为二进制
        bind_ret = bind(fd,(const struct sockaddr *)&sa_in,sizeof(sa_in));  //为监听描述符绑定ip和port,也被称为命名
        if(bind_ret < 0)
        {
                perror("bind");
                exit(-1);
        }
        debug("bind success\n");

        listen_ret = listen(fd,LISTEN_NUM);  //设置socket为监听状态
        if(listen_ret < 0)
        {
                perror("listen");
                exit(-1);
        }
        debug("listening\n");
        while(1)  //在循环中处理连接请求
        {
                accept_ret = (int *)malloc(sizeof(int));  //动态分配的原因:避免读写描述符被多个线程混用
                *accept_ret = accept(fd,(struct sockaddr *)&client_sa,&client_len);  //从链接队列中获取建立好的socket连接
                if(accept_ret < 0)
                {
                        perror("accept");
                        exit(-1);
                }
                printf("accept_ret,fd for client:%d\n",*accept_ret);

                ret = pthread_create(&thread,NULL,func,(void *)accept_ret);  //为连接创建线程
                if(ret != 0)
                {
                        perror("pthread_create");
                        exit(-1);
                }
        }

        return 0;
}

void *func(void *fd) //线程处理函数
{
        int *accept_ret = (int *)fd;
        char receivebuf[20] = {0};
        int recv_ret = -1;

        while(1)
        {
                recv_ret = recv(*accept_ret,receivebuf,sizeof(receivebuf),0);//接收客户端数据
                if(recv_ret < 0)
                {
                        perror("recv");
                        exit(-1);
                }
                }
                else if(recv_ret == 0)  //连接断开
                {
                        if(close(*accept_ret)) //关闭读取描述符
                        {
                                perror("close");
                                exit(-1);
                        }
                        printf("close:%d,exiting subthread\n",*accept_ret);
                        free(accept_ret);  //释放动态分配的描述符空间
                        pthread_exit(NULL);//线程返回
                }
                printf("%d bytes from %d,%s\n",recv_ret,*accept_ret,receivebuf);
                memset(receivebuf,0,strlen(receivebuf));
        }
}

void sigint_handle(int sig) //信号处理函数
{
        if(SIGINT != sig)return;
        printf("\nserver termated\n");
        close(fd);
        exit(1);
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>

#define DEBUG
#ifdef DEBUG
#define debug(x) printf(x)
#else
#define debug(x)
#endif

#define SERVER_IP "192.168.150.129"
#define SERVER_PORT 1500

typedef void (*sighandler_t)(int);

void sigpipe_handle(int arg); //sigpipe信号处理函数,当send或是recv在等待发送或是接收数据时发现连接断开,系统会发出该信号

static int fd = -1;

int main(int argc,char *argv[])
{
        struct sockaddr_in server_sa = {0};
        int connect_ret = -1;

        int send_ret = -1;

        sighandler_t sig_ret = (sighandler_t)-1;

        fd = socket(AF_INET,SOCK_STREAM,0);   //建立socket连接,设置连接类型
        if(fd < 0)
        {
                perror("socket");
                exit(-1);
        }
        printf("fd:%d\n",fd);

        sig_ret = signal(SIGPIPE,sigpipe_handle);  //为sigpipe信号绑定处理函数
        if(SIG_ERR == sig_ret)
        {
                perror("signal");
                exit(-1);
        }

        server_sa.sin_port = htons(SERVER_PORT);
        server_sa.sin_addr.s_addr = inet_addr(SERVER_IP);
        server_sa.sin_family = AF_INET;
        connect_ret = connect(fd,(struct sockaddr *)&server_sa,sizeof(server_sa));  //向服务器发送连接请求
        if(connect_ret < 0)
        {
                perror("connect");
                exit(-1);
        }
        debug("connect done\n");

        char sendbuf[20] = {0};
        while(1)    //将用户输入的数据发送给服务器端,当输入end时,客户端退出
        {
                printf("input your data\n");
                scanf("%s",sendbuf);
                if(!strncmp(sendbuf, "end",3))
                {
                        debug("exiting....\n");
                        close(fd);
                        return 0;
                }

                send_ret = send(fd,sendbuf,strlen(sendbuf),0);  //向服务器发送信息
                if(send_ret < 0)
                {
                        perror("send");
                        exit(-1);
                }
                printf("data sent successfully:%d\n",send_ret);
        }
}

void sigpipe_handle(int arg)  //打印提示信息后再退出
{
        if(SIGPIPE == arg)
        {
                printf("server disconnect\n");
                close(fd);
                debug("exiting\n");
                exit(1);
        }
}

send函数


 ssize_t  send(int  socket, const  void  *buffer, size_t length, int flags);

      不论是客户端还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
参数:

  1.  第一个参数指定发送端套接字描述符;
  2.  第二个参数指明一个存放应用程序要发送数据的缓冲区;
  3.  第三个参数指明实际要发送的数据的字节数;
  4.  第四个参数用于改变函数的行为。
  • 0:与write()无异,一般置0
  • MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
  • MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
  • MSG_OOB:指明发送的是带外信息

 

        这里只描述同步Socket的send函数的执行流程。当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len,如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,如果len小于剩余空间大小send就仅仅把buf中的数据 copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。
        如果send函数copy数据成功就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
注意: 

  •  send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回SOCKET_ERROR)
  •  在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。


recv函数


 ssize_t recv(int socket, void *buffer, size_t length, int flags);

 不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。 

  1.  第一个参数指定接收端套接字描述符;
  2.  第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
  3.  第三个参数指明buf的长度;
  4.  第四个参数用于改变函数的行为。
  • 0:与read()相同,一般置0
  • MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
  • MSG_OOB:指明发送的是带外信息;在网络上有两种类型的数据包,正常包和带外包。带外包可以通过检验一个TCP/IP包头的一个特定标志来决定。
  • MSG_PEEK: 可以查看可读的信息,在接收数据后不会将这些数据丢失;从输入数据中取数。数据拷入缓冲区,但不从输入队列中移走。函数返回当前准备接收的字节数。
  • MSG_WAITALL:通知内核直到读到请求的数据字节数时,才返回。


        这里只描述同步Socket的recv函数的执行流程。当应用程序调用recv函数时,recv等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv则去检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0
 注意

  • 在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。这与send函数一样,我们可以捕获该信号,在程序退出前做一些我们需要的处理。

 

根据上面的说明理解下面一段话:

发送方与接收方没有定制协议的情况下,接收方不可能知道发送方是否已经发送完毕,如果想要在这种情况下实现接收方接收发送方多次send()数据可以在recv时加个参数,让它只是看看有没有数据到达,recv最后那个参数设为MSG_OOB就可以,当发现有数据到时,你先Sleep一下,至于多长时间就要看实际情况,一般几十个毫秒就绝对够了,然后,你再recv一下,这次最后那个参数为MSG_WAITALL就可以,这样就一次性接收完毕

 

backlog参数

int listen(int socket, int backlog);

listen函数仅由TCP服务器调用,它做两件事情:

1、当socket函数创建一个套接口时,它被假设为一个主动套装口,也就是说,它是一个将调用connet发起连接的客户套接口。listen函数把一个未连接的套接口转换成一个被动套接口,指示内核应接受指向该套接口的连接请求。根据TCP状态转换图,调用listen导致套接口从CLOSED状态转换到LISTEN状态。

2、backlog其实是一个连接队列,以下是backlog队列大小公式。backlog队列总和=未完成三次握手队列 +  已经完成三次握手队列
 
以上参数解释说明如下:
 
未完成三次握手队列:服务器处于listen状态时收到客户端syn 报文(connect)时放入未完成队列中。
 
已经完成三次握手队列:三次握手的第二个状态即服务器syn+ ack响应client后,到第三个状态ack报文到达前(客户端对服务器syn的ack)一直保留在未完成连接队列中,如果三次握手完成,该条目将从未完成连接队列搬到已完成连接队列尾部.
 
backlog参数设置既可以在linux内核参数设置(修改文件/etc/sysctl相关参数),也可以在socket系统调用listen函数时设置(第二个参数),这二者区别是,前者为全局性的,影响所有socket,后者为局部性的,影响当前socket。
只要accept函数保持快速的响应,该参数并不会被触发,若在accpet之后进行sleep,则队列中的连接数量可能会超过该参数的值。
  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值