手把手带你用Linux socket写出聊天室demo

5 篇文章 0 订阅
1 篇文章 0 订阅

Linux网络编程篇之Tcp协议介绍, C/S通信及聊天室实现

基于tcp协议的网络程序流程图如下:

tcp协议网络程序流程图

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态

客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答

服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

一. 协议流程分析

建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段

二. 简单例子(单线程实现客户端与服务端的简单通信)

实现功能: 客户端连接服务端后,不断从控制台读取字符串,发给服务端,服务端接收后则在控制台界面输出

tcp_server.c

/*
 * @Author: D-lyw 
 * @Date: 2018-10-25 00:48:44 
 * @Last Modified by: D-lyw
 * @Last Modified time: 2018-11-16 12:36:34
 */

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

#define SERVADDR_PORT 8800

const char *LOCALIP = "127.0.0.1";

int main(int argc, char const *argv[])
{
    // 定义变量存储生成或接收的套接字描述符
    int listenfd, recvfd;
    // 定义一个数据结构用来存储套接字的协议,ip,端口等地址结构信息
    struct sockaddr_in servaddr, clientaddr;
    // 定义接收的套接字的数据结构的大小
    unsigned int cliaddr_len, recvLen;
    char recvBuf[1024];

    //创建用于帧听的套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    // 给套接字数据结构赋值,指定ip地址和端口号
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVADDR_PORT);
    servaddr.sin_addr.s_addr = inet_addr(LOCALIP);

    // 绑定套接字
    if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){
        fprintf(stderr, "绑定套接字失败,%s\n", strerror(errno));
        exit(errno);
    }

    // 监听请求
    if(listen(listenfd, 10) == -1){
        fprintf(stderr, "绑定套接字失败,%s\n", strerror(errno));
        exit(errno);
    }

    cliaddr_len = sizeof(struct sockaddr);

    // 等待连接请求
    while (1){
        // 接受由客户机进程调用connet函数发出的连接请求
        recvfd = accept(listenfd, (struct sockaddr *)&clientaddr, &cliaddr_len);
        printf("接收到请求套接字描述符: %d\n", recvfd);

        while(1){
            // 在已建立连接的套接字上接收数据
            if((recvLen = recv(recvfd, recvBuf, 1024, 0)) == -1){
                fprintf(stderr,"接收数据错误, %s\n",strerror(errno));
            }
            printf("%s", recvBuf);
        }
    }
    close(recvfd);
    return 0;
}

tcp_client.c

/*
 * @Author: D-lyw 
 * @Date: 2018-10-26 14:06:32 
 * @Last Modified by: D-lyw
 * @Last Modified time: 2018-11-16 12:34:08
 * @name tcp_client.c
 * @descripe    实现最基本的创建套接字, 填充客户端信息,connet连接服务端, 可连续向服务端发送消息
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <string.h>
extern int errno;

#define SERVERPORT 8800

int main(int argc, char const *argv[])
{
    // 定义变量存储本地套接字描述符
    int clifd;
    // 设置本地ip地址
    const char serverIp[] = "127.0.0.1";
    // 定义套接字结构存储套接字的ip,port等信息
    struct sockaddr_in cliaddr_in;
    // 定义发送,接收缓冲区大小
    char sendBuf[1024], recvBuf[1024];

    // 创建套接字
    if((clifd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        fprintf(stderr, "创建套接字失败,%s\n", strerror(errno));
        exit(errno);
    }

    // 填充 服务器端结构体信息
    cliaddr_in.sin_family = AF_INET;
    cliaddr_in.sin_addr.s_addr = inet_addr(serverIp);
    cliaddr_in.sin_port = htons(SERVERPORT);

    // 请求连接服务器进程
    if(connect(clifd, (struct sockaddr *)&cliaddr_in, sizeof(struct sockaddr)) == -1){
        fprintf(stderr,"请求连接服务器失败, %s\n", strerror(errno));
        exit(errno);
    }
    strcpy(sendBuf, "hi,hi, severs!\n");
    // 发送打招呼消息
    if(send(clifd, sendBuf, 1024, 0) == -1){
        fprintf(stderr, "send message error:(, %s\n", strerror(errno));
        exit(errno);
    }
    // 阻塞等待输入,发送消息
    while(1){
        fgets(sendBuf, 1024, stdin);
        if(send(clifd, sendBuf, 1024, 0) == -1){
            fprintf(stderr, "send message error:(, %s\n", strerror(errno));
        }
    }
    close(clifd);
    return 0;
}

三. 聊天室功能实现(多线程)

服务器端代码

/*
 * @Author: D-lyw 
 * @Date: 2018-11-22 20:37:05 
 * @Last Modified by: D-lyw
 * @Last Modified time: 2018-11-23 00:19:42
 * @Describe Chating Room Coded by linux c .
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>

const unsigned short LOCALPORT = 3003;
const char *LOCALIP = "127.0.0.1";
#define MAXMSGSIZE 1024*5
#define MAXCONNECT 100

char sendbuf[MAXMSGSIZE];
char recvbuf[MAXMSGSIZE];

struct msgHdr{
    int fd;                     // 套接字描述符
    ushort tip;                 // 0 进入聊天室,    1 离开聊天室    2 发送消息
    ushort onLineNum;           // 在线人数
};

// 线程描述符
pthread_t Precv;
pthread_t Psend;

int clientFdarray[MAXCONNECT];
struct msgHdr *sendMsgHdr, *recvMsgHdr;
int onLineNum = 0;      // 在线人数

void rmFd(int dealfd){
    for(int i = 0; i < (onLineNum+1); i++){
        if(clientFdarray[i] == dealfd){
            for(; i < onLineNum; i++){
                clientFdarray[i] = clientFdarray[i+1];
            }
            printf("当前所有用户:\n");
            for(int j = 0; j < onLineNum; j++){
                printf("     用户:%d\n", clientFdarray[j]);
            }
        }
    }
}

// 服务器发送消息线程
void sendToClient(char *buf, int dealfd){
    for(int j = 0; j < onLineNum; j++){
        if(clientFdarray[j] == dealfd){
            continue;
        }
        if(send(clientFdarray[j], buf, MAXMSGSIZE, 0) == -1){
            fprintf(stderr, "%s\n", strerror(errno));
        }
    }
    bzero(buf, MAXMSGSIZE);
}

// 接收客户端消息线程
void *recvMsg(void *recvfd){
    int dealfd = *(int *)recvfd;
    while(1){
        if(recv(dealfd, recvbuf, MAXMSGSIZE, 0) == -1){
            fprintf(stderr, "Receive msg err: %s\n", strerror(errno));
        }
        recvMsgHdr = (struct msgHdr *)recvbuf;
        recvMsgHdr->fd = dealfd;
        if(recvMsgHdr->tip == 1){
            onLineNum--;
            recvMsgHdr->onLineNum = onLineNum;
            // 将离开的套接字描述符移开在线列表数组
            rmFd(dealfd);
            printf("用户:%d 离开了聊天室\n", dealfd);
            sendToClient(recvbuf, dealfd);
            close(dealfd);
            return NULL;
        }
        // 将此用户的消息发给其他用户
        sendToClient(recvbuf, dealfd);
    }
    close(dealfd);
    return NULL;
}

int main(int argc, char const *argv[])
{
    int serverfd, recvfd;
    socklen_t sockleng;
    struct sockaddr_in serveraddr, clientaddr;
    pid_t childid;
    int perrno;

    if( (serverfd = socket(AF_INET, SOCK_STREAM, 0) ) == -1){
        fprintf(stderr, "创建服务器套接字错误, %s\n", strerror(errno));
        exit(0);
    }

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(LOCALPORT);
    serveraddr.sin_addr.s_addr = inet_addr(LOCALIP);

    if(bind(serverfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr)) == -1){
        fprintf(stderr, "绑定套接字错误, %s\n", strerror(errno));
        exit(0);
    }

    if(listen(serverfd, 100) == -1){
        fprintf(stderr, "监听套接字错误, %s\n", strerror(errno));
    }
    
    printf("\nListening at %d port, wating connection.....\n", LOCALPORT);

    while(1){
        if((recvfd = accept(serverfd, (struct sockaddr *)&clientaddr, &sockleng)) == 0){
            fprintf(stderr, "连接错误, %s\n", strerror(errno));
            continue;
        }
        // 将该套接字描述符保存进数组
        clientFdarray[onLineNum++] = recvfd;

        printf("客户端套接字:%d 已开启\n", recvfd);

        sendMsgHdr = (struct msgHdr *)sendbuf; 
        sendMsgHdr->fd = recvfd;
        sendMsgHdr->tip = 0;
        sendMsgHdr->onLineNum = onLineNum;

        // 当有用户加入时,通知聊天室中的所有人
        for(int j = 0; j < onLineNum; j++){
            if(send(clientFdarray[j], sendbuf, MAXMSGSIZE, 0) == -1){
                fprintf(stderr, "%s\n", strerror(errno));
            }
        }
        bzero(sendbuf, MAXMSGSIZE);

        // 创建接收用户消息处理线程
        if((perrno = pthread_create(&Precv, NULL, recvMsg, &recvfd)) != 0){
            fprintf(stderr, "创建子消息接收线程失败, %s\n", strerror(perrno));
            exit(perrno);
        }
    }
    close(serverfd);
    return 0;
}

客户端代码

/*
 * @Author: D-lyw 
 * @Date: 2018-11-22 21:47:58 
 * @Last Modified by: D-lyw
 * @Last Modified time: 2018-11-23 00:37:01
 */

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

extern int errno;

#define MAXSIZE 1024*5
#define SERVER_PORT 3003
const char *SERVERIP = "127.0.0.1";
// const char *SERVERIP = "120.78.156.5";

char sendbuf[MAXSIZE];
char recvbuf[MAXSIZE];

struct msgHdr{
    int fd;                     // 套接字描述符
    ushort tip;                 // 0 进入聊天室,    1 离开聊天室    2 发送消息
    ushort onLineNum;           // 在线人数

};

int sockfd;
struct msgHdr *mySendMsgHdr, *myRecvMsgHdr;

void *sendMsg(void *msg){
    while(1){
        mySendMsgHdr = (struct msgHdr *)sendbuf;
        mySendMsgHdr->fd = sockfd;
        
        fgets(sendbuf + sizeof(struct msgHdr), MAXSIZE - sizeof(struct msgHdr), stdin);
        
        if(strncmp(sendbuf + sizeof(struct msgHdr), "end", 3) == 0){    // 用户离开聊天室
            mySendMsgHdr->tip = 1;      
            if(send(sockfd, sendbuf, MAXSIZE, 0) == -1){
                fprintf(stderr, "%s\n", strerror(errno));
            }
            close(sockfd);
            exit(0);
        }else{
            mySendMsgHdr->tip = 2;              // 用户发送数据
        }
        
        if(send(sockfd, sendbuf, MAXSIZE, 0) == -1){
            fprintf(stderr, "%s\n", strerror(errno));
        }
        bzero(sendbuf, MAXSIZE);
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    ssize_t sendLen;
    struct sockaddr_in seraddr, recvaddr;
    pthread_t Psend;
    

    // 创建一个客户端的套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        fprintf(stderr, "%s\n", strerror(errno));
        exit(errno);
    }

    bzero(&seraddr, sizeof(struct sockaddr_in));
    // 服务器端地址信息
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = inet_addr(SERVERIP);
    seraddr.sin_port = htons(SERVER_PORT);

    // 请求连接服务器进程
    if(connect(sockfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr)) == -1){
        fprintf(stderr,"请求连接服务器失败, %s\n", strerror(errno));
        exit(errno);
    }
    
    printf("--------Successful connect to %s:%d--------\n", inet_ntoa(seraddr.sin_addr), ntohs(seraddr.sin_port));

    // 新建线程发送消息
    pthread_create(&Psend, NULL, sendMsg, NULL);
    
    // 接收其他用户消息
    while(1){
        // 清空缓存区
        bzero(recvbuf, MAXSIZE);
        if(recv(sockfd, recvbuf, MAXSIZE, 0) == -1){
            fprintf(stderr, "%s\n", strerror(errno));
        }
        
        myRecvMsgHdr = (struct msgHdr *)recvbuf;
        if(myRecvMsgHdr->tip == 0){
            fprintf(stdout, "        **用户 %d 加入聊天室 当前用户: %d 人**        \n", myRecvMsgHdr->fd, myRecvMsgHdr->onLineNum);
        }else if(myRecvMsgHdr->tip == 1){
            printf("       **用户 %d 离开聊天室 当前用户: %d 人**         \n", myRecvMsgHdr->fd, myRecvMsgHdr->onLineNum);
        }else if(myRecvMsgHdr->tip == 2){
            fprintf(stdout, "#%d> %s\n", myRecvMsgHdr->fd, recvbuf+sizeof(struct msgHdr));
        }
        
    }
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值