socket编程:简单的多客户端聊天室

服务器:

#include "chat.h"
void *stranspond(void *arg);

int                 fd_A[MAX_CLIENT];
int                 conn_amount;

struct user_list    user;

int main()
{
    int                     server_fd;
    int                     newfd;
    int                     i;
    int                     result;
    int                     maxsock;
    int                     sin_size;
    char                    buffer[BUFFER_SIZE];

    struct user_list        *tmp;
    struct list_head        *pos;
    struct list_head        *q;
    struct sockaddr_in      s_add;
    struct sockaddr_in      c_add;
/*
struct timeval
{
__time_t tv_sec;        秒
__suseconds_t tv_usec;  微秒
};
 */
    struct timeval          tv;

    pthread_t               a_thread;
    fd_set                  set_fd;
    /*初始化链表头*/
    INIT_LIST_HEAD(&user.mylist);

    printf("hello, welcome to my server! \n");
/*
初始化一个用于监听的socket,AF_INET表示的是TCP/IP协议族,SOCK_STREAM代表的是数据流。第二个参数还包括数据报SOCK_DGRAM。
 */
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        printf("socket fail\n");
        return -1;
    }

    printf("socket ok\n");
/*
bzero()函数用于套接字地址清零。INADDR_ANY表示的是本机网口的一个地址。AF_INET表示的是TCP/IP协议族。
 */
    bzero(&s_add, sizeof(struct sockaddr_in));
    s_add.sin_family        = AF_INET;
    s_add.sin_addr.s_addr   = INADDR_ANY;
    s_add.sin_port          = htons(MYPORT);
    /*绑定监听socket*/
    if (bind(server_fd, (struct sockaddr *)(&s_add), sizeof(struct sockaddr)) == -1) {
        printf("bind fail \n");
        return -1;
    }

    printf("bind ok \n");
    /*设置监听socket,MAX_CLIENT表示最大的监听数,当连接数目超过MAX_CLIENT时拒绝连接*/
    if (listen(server_fd, MAX_CLIENT) == -1) {
        printf("listen fail \n");
        return -1;
    }

    printf("listen ok \n");
    conn_amount = 0;
    sin_size    = sizeof(struct sockaddr_in);
    maxsock     = server_fd;

    while(1)
    {   
        /*初始化fd_set集合*/
        FD_ZERO(&set_fd);
        FD_SET(server_fd, &set_fd);
        tv.tv_sec    = 10;
        tv.tv_usec   = 0;
/*判断描述符fd是否在给定的描述符集fdset中,通常配合select函数使用,由于select函数成功返回时会将未准备好的描述符位清零。通常我们使用FD_ISSET是为了检查在select函数返回后,某个描述符是否准备好,以便进行接下来的处理操作。
当描述符fd在描述符集fdset中返回非零值,否则,返回零。
 */
        if (FD_ISSET(server_fd, &set_fd)) {
/*
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数,传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。
 */
            newfd = accept(server_fd, (struct sockaddr *)&c_add, &sin_size);
            if (newfd <= 0) {
                printf("accept fail\n");
            }

            if (conn_amount < MAX_CLIENT) {
                fd_A[conn_amount] = newfd;
                printf("new connection client[%d] %d:%d\n",
                    conn_amount, ntohl(c_add.sin_addr.s_addr), c_add.sin_port);
/*
使用用户信息链表,user_list,将客户端的信息存储到链表中。struct user_list *tmp,需要分配tmp空间,注意tmp 为一个指针。
 */
                tmp = (struct user_list *)malloc(sizeof(struct user_list));
                tmp->cli_addr                   = c_add;
                tmp->cli_sock_fd                = fd_A[conn_amount];
                tmp->server_sock_fd             = server_fd;
                tmp->cli_addr.sin_addr.s_addr   = inet_addr("127.0.0.1");
                tmp->amount                     = conn_amount;

                list_add_tail(&tmp->mylist, &user.mylist);
                conn_amount++;

                if (newfd > maxsock) {
                    maxsock = newfd;
                }
            } else {
                close(newfd);
            }
        }

        for (i = 0; i < MAX_CLIENT; i++) {
            if(fd_A[i] != 0) {
                FD_SET(fd_A[i], &set_fd);
            }
        }
/*
FD_ISSET()函数判断监听描述符是否准备好。
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
参数说明: 
int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1。
fd_set *readfds 是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的。如果有一个文件可读,返回大于0。如果没有可读文件,再根据timeout查看是否超时,超时返回0。如果返回负值,则表示select错误。
fd_set *writefds    也是指向fd_set结构的指针,监视写变化。返回值大于0可写。若没有可写,查看timeout是否超时,超时返回0.返回0表示select错误。
fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。 
struct timeval* timeout是select的超时时间,它可以使select处于三种状态。
    一、传入NULL表示将select设置为阻塞状态,一直等到文件描述符集合中某个描述符发生变化。
    二、传入时间为00毫秒,将select设置为非阻塞状态,不管文件描述符是否发生变化,都立刻返回执行。文件描述符无变化返回0,有变化返回一个正值。
    三、将timeout设置为大于0的值。select在timeout内阻塞,有事件描述符发生变化时返回大于0的值。超时后,不管是否发生变化,都立刻执行返回。又变化返回正值,无变化返回0*/
        if (FD_ISSET(server_fd, &set_fd)) {
            printf("wait a qq come:\n");
            result = select(maxsock + 1, &set_fd, NULL, NULL, &tv);
            if (result < 0) {
                printf("select fail");
                break;
            } else if (result == 0) {
                printf("time out\n");
                continue;
            } else {
                for (i = 0; i < conn_amount; i++) {
                    if (FD_ISSET(fd_A[i], &set_fd)) {
/*
int send( SOCKET s, const char FAR *buf, int len, int flags );  
参数说明:
    第一个参数指定发送端套接字描述符;
    第二个参数指明一个存放应用程序要发送数据的缓冲区;
    第三个参数指明实际要发送的数据的字节数;
    第四个参数一般置0。 
不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。需要对应使用。
套接字s中有一个发送缓冲区。如果len > s的发送缓冲区,则返回SOCKET_ERROR。如果len <= s发送缓冲区,检查是否在发送中。
    是:把协议中的数据发送完
    否:s的发送缓冲区未开始发送,或者s发送缓冲区无数据。
    比较len与s的剩余空间:
        len > s send等待s缓冲区发送完
        len < s 把buf中的数据拷贝到缓冲区中
int recv( SOCKET s, char FAR *buf, int len, int flags);   
参数说明:
    第一个参数指定接收端套接字描述符;
    第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
    第三个参数指明buf的长度;
    第四个参数一般置0。
不论是客户还是服务器应用程序都用recv函数来向TCP连接的另一端接收数据。
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。
 */                 
                        result = recv(fd_A[i], tmp->qq_num_from, sizeof(tmp->qq_num_from), 0);
                        printf("wait anther qq:\n");
                        recv(fd_A[i], tmp->qq_num_to, sizeof(tmp->qq_num_to), 0);
/*
int  pthread_create((pthread_t  *thread,  pthread_attr_t  *attr,  void  *(*start_routine)(void  *),  void  *arg)
主要,是creat**e**!
参数说明:
    第一个参数为指向线程标识符的指针。
    第二个参数用来设置线程属性。一般设置为NULL.
    第三个参数是线程运行函数的起始地址。
    最后一个参数是运行函数的参数。只能是一个参数!
 */                        
                        result = pthread_create(&a_thread, NULL, stranspond, (void *)tmp);
                        if (result != 0) {
                            perror("pthread_creat");
                        }
                        printf("you qq:%s: she qq:%s\n", tmp->qq_num_from, tmp->qq_num_to);
                    }
                }
            }
        }
    }
}
/*
该线程用于数据转发
 */
void *stranspond(void *arg)
{
    (void) signal(SIGINT, ouch);

    int                         send_fd;
    int                         server_fd;
    int                         result;
    int                         file_in;
    int                         new_sock;
    int                         loop = 1;
    char                        *from;
    char                        *to;
    char                        buffer[BUFFER_SIZE];
    char                        buffer2[BUFFER_SIZE];
    char                        warn_wait[] = "user:%s     don't connect!please wait 5 second\n";
    const char                  notes[] = "%s:\n    %s";
    const char                  user_file[] = "./log/%s.in";
    char                        warn[64];
    char                        log_name[21];
    int                         send_len;

    struct sockaddr_in          cli_addr;
    struct list_head            *she_list;
    struct user_list            *client;
    struct user_list            *unit_data;

    client      = (struct user_list *)arg;
    from        = (*client).qq_num_from;
    to          = (*client).qq_num_to;
    send_fd     = (*client).cli_sock_fd;
    server_fd   = (*client).server_sock_fd;
    cli_addr    = (*client).cli_addr;
/*将分配的端口加1,*/
    (*client).cli_addr.sin_port++;

    sprintf(log_name, user_file, from);
    printf("log_name=%s\n", log_name);
    file_in = open(log_name, O_CREAT|O_WRONLY, S_IWUSR|S_IXOTH);
    if(file_in < 0) {
        printf("open fail\n");
    }

    send(send_fd, "i love you", 11, 0);
    send_len = send(send_fd, client, sizeof(struct user_list), 0);
    printf("you send :%d bytes client.qq_num_from=%s \n", send_len, from);
    printf("connecting......\n");

/*创建一个新的socket用于连接客户端的socket*/
    new_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (connect(new_sock, (struct sockaddr *)(&cli_addr), sizeof(struct sockaddr)) == -1) {
        printf("connect fail \n");
    }

    printf("you connect qq=%s---port=%d\n", from, (*client).cli_addr.sin_port);
    sprintf(warn, warn_wait, to);
    client->cli_receive_fd = new_sock;

/*
查找发送对象是否在线。
list_for_each(pos, head) 从head节点开始(不包括head节点!)遍历它的每一个节点。
list_entry(ptr, type, member) 获取type类型结构体的起始指针。

 */
    while (loop) {
        list_for_each(she_list, &user.mylist) {
            unit_data = list_entry(she_list, struct user_list, mylist);
            if(strcmp(to, unit_data->qq_num_from) == 0) {
                loop = 0;
                break;
            }
        }
        printf("%s", warn);
        send(new_sock, warn, sizeof(warn), 0);
        sleep(5);
    }

    send(new_sock, "she is connect !\n", sizeof("she is connect !\n"), 0);
    printf("you qq:%s she qq:%s and cli_receive_fd=%d\n",
        from, to, client->cli_receive_fd);
    printf("new_sock = %d,", new_sock);

    while(strncmp("end", buffer, 3) != 0)
    {
        result = recv(new_sock, buffer, sizeof(buffer), 0);
        if(result < 0) {
            printf("recv error!\n");
        }

        sprintf(buffer2, notes, from, buffer);
        write(file_in, buffer2, sizeof(buffer2));
        send_len = send(unit_data->cli_receive_fd, buffer2, sizeof(buffer), 0);
        if (send_len < 0) {
            printf("send to she error");
            pthread_exit(NULL);
        }

        if(buffer[0] != '\0') {
            printf("\nQQ:%s  send:%s", from, buffer2);
        }
    }

    list_del(she_list);
    free(unit_data);
    close(file_in);
    close(new_sock);
    close(send_fd);
    conn_amount--;
    pthread_exit(NULL);
}

客户端:

#include "chat.h"

void *receive(void *arg);

int         new_fd;
int         receive_fd;
pthread_t   reveive_pthread;

int main()
{
    int                 client_fd;
    int                 result;
    int                 sen;
    char                qq_num[10];
    char                qq_num2[10];
    char                buffer[BUFFER_SIZE];
    struct user_list    me;
    struct sockaddr_in  s_add;
    unsigned short      portnum = 0x8899;

    printf("Hello,welcome to client \n");

    client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd == -1) {
        printf("socket fail \n");
        return -1;
    }
    printf("socket ok \n");

/*s_add为server监听socket的地址与端口*/
    bzero(&s_add, sizeof(struct sockaddr_in));
    s_add.sin_family        = AF_INET;
    s_add.sin_addr.s_addr   = inet_addr("127.0.0.1");
    s_add.sin_port          = htons(portnum);
    printf("s_addr = %#x ,port = % \n",s_add.sin_addr.s_addr,s_add.sin_port);
/*
 int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);
 sock_fd是需要进行连接的描述符。serv_addr是服务端监听socket的地址指针。addrlen是结构sockaddr_in的长度。
 成功返回0,失败返回-1,并设置errno。
 */
    result = connect(client_fd, (struct sockaddr *)(&s_add), sizeof(struct sockaddr));
    if (result == -1) {
        printf("connect fail \n");
    }

    printf("you connect server\n");
    printf("please enter you qq number:\n");
    fgets(qq_num, 10, stdin);
    sen = strlen(qq_num);
    qq_num[sen-1] = '\0';
    sen = send(client_fd, &qq_num, strlen(qq_num), 0);
    if(sen < 0) {
        printf("send fail\n");
    }

    printf("please enter you she number:\n");
    fgets(qq_num2, 10, stdin);
    sen = strlen(qq_num2);
    qq_num2[sen - 1] = '\0';
    sen = send(client_fd, &qq_num2, strlen(qq_num2), 0);
    if(sen < 0) {
        printf("send fail\n");
    }

    printf("send qq ok\n");
/*
extern void *memset(void *buffer, int c, int count) 
buffer为指针或是数组,c是赋给buffer的值,count是buffer的长度.
这个函数在socket中多用于清空数组.
 */
    memset(buffer, 0, sizeof(buffer));
    recv(client_fd, buffer, sizeof(buffer), 0);
    printf("%s\n",buffer);
/*
得到一个新的user_list结构me,使用me中的cli_addr进行地址绑定,启动一个新的线程专门用于接收消息
recv()函数不仅可以用于接收数组,也可以用于接收其他数据类型,例如结构。但是要特别注意,两边的数据长度一定要相同,避免数据的丢失。之前修改了server中的user_list却没有修改client的结构user_list,导致bind失败,原因是后面地址位丢失。
*/
    int recv_len = recv(client_fd, &me, sizeof(me), 0);
    if (recv_len < 0) {
        printf("recv mylist error!");
    }

    printf("recv %d bytes \n new_port = %d\n", recv_len, me.cli_addr.sin_port);
    new_fd = socket(AF_INET, SOCK_STREAM, 0);
    result = bind(new_fd, (struct sockaddr *)(&me.cli_addr), sizeof(struct sockaddr));
    if (result == -1) {
        printf("bind fail \n");
    }

    result = listen(new_fd, MAX_CLIENT);
    if (result == -1) {
        printf("listen fail \n");
    }

    int sin_size = sizeof(struct sockaddr_in);
    receive_fd = accept(new_fd, (struct sockaddr *)(&me.cli_addr), &sin_size);
    if (receive_fd <= 0) {
    printf("accept fail\n");
    }

    result = pthread_create(&reveive_pthread, NULL, receive, NULL);
    if (result == -1) {
        printf("pthread_create fail\n");
    }

    printf("Now! you can chat with she\n");     /*send model*/
    while(strncmp("end", buffer, 3) != 0)
    {
        fgets(buffer, BUFFER_SIZE, stdin);
        if(buffer[0] != '\0') {
            sen = send(receive_fd, buffer, sizeof(buffer), 0);
            if(sen < 0) {
                printf("send fail\n");
            }
            printf("\nQQ:%s  :%s", qq_num, buffer);
        }
    }

    close(client_fd);
    close(receive_fd);
    printf("i am dead \n");
    exit(EXIT_SUCCESS);
    return 0;
}

void *receive(void *arg)
{
    (void) signal(SIGINT, ouch);

    char    buffer[BUFFER_SIZE];
    int     result;
    while(1)
    {
        result = recv(receive_fd, buffer, sizeof(buffer), 0);
        if(result < 0) {
              printf("recv error!\n");
              exit(EXIT_FAILURE);
        }

        if(buffer[0] != '\0') {
            printf("%s", buffer);
        }

    }
    exit(EXIT_SUCCESS);
}

头文件:

#ifndef __CHAT_H
#define __CHAT_H

#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "list.h"

#define MYPORT          0x8899
#define MAX_CLIENT      5
#define BUFFER_SIZE     1024
#define NAME_SIZE       10

struct user_list{
        int                 cli_sock_fd;
        int                 cli_receive_fd;
        int                 server_sock_fd;
        int                 amount;
        char                qq_num_from[NAME_SIZE];
        char                qq_num_to[NAME_SIZE];
        struct sockaddr_in  cli_addr;
        struct list_head    mylist;
};
void ouch(int sig)
{
    printf("ouch! i got signal %d\n", sig);
    (void) signal(SIGINT, SIG_DFL);
    kill(getpid(), SIGINT);
}
#endif

“`

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值