一、需求定义

本课程设计是在Linux环境下基于Socket进行开发的。因为之前也学过计算机网络原理,接触过TCP/UDP 这些东西,所以就想试着写一个聊天室的功能出来。通过之前的学习,我知道了系统服务器和客户瑞组成。服务端程序通过共享存储区存储聊天数据,并发送给每个连接的客产端。通过多路复用的子进程实现服务端与多个客户端之间的数据发送与接收。可以在单机上开将两个该口分别运行客户、服务器的程序。经linux下gcc调试成功,可以实现简单的群聊效果。关键词:网络聊天,linux ,socket.相关概念及技术

实现语言:C语言

二、分析设计

图片转载

 

  1. 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
  2. 然后客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
  3. 客户端通过提示,输入用户名,start(),方法回显是否成功加入聊天室(最多支持五十个客户端)。
  4. 客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据(注:我是服务器和客户端通过 send() 和 recv() 进行通信)。
  5. 最后关闭连接,一次交互结束。
  6. 服务器读取信息。
  7. 客户端关闭。
  8. 服务器端关闭。

三、代码实现和测试

1.开发环境

VmWare+Centos7.5

  1. 客户端的代码(源码)

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <pthread.h>

#include <time.h>

int clientfd2;//客户端socket

char* IP = "127.0.0.1";//服务器的IP

short PORT = 6666;//服务器服务端口

typedef struct sockaddr meng;

char name[30];//设置支持的用户名长度

time_t nowtime;

void init(){

    clientfd2 = socket(PF_INET,SOCK_STREAM,0);//创建套接字

    struct sockaddr_in addr;//将套接字存在sockaddr_in结构体中

    addr.sin_family = PF_INET;//地址族

    addr.sin_port = htons(PORT);//端口号 可随意设置,不过不可超过规定的范围

    addr.sin_addr.s_addr = inet_addr(IP);//inet_addr()函数将点分十进制的字符串转换为32位的网络字节顺序的ip信息

//发起连接

    if (connect(clientfd2,(meng*)&addr,sizeof(addr)) == -1){

        perror("无法连接到服务器");

        exit(-1);

    }

    printf("客户端启动成功\n");

}

void start(){

    pthread_t id;

    void* recv_thread(void*);

//创建一个线程用于数据的接收,一个用于数据的发送

    pthread_create(&id,0,recv_thread,0);

    char buf2[100] = {};

    sprintf(buf2,"%s进入了群聊",name);

    time(&nowtime);

    printf("进入的时间是: %s\n",ctime(&nowtime));

    send(clientfd2,buf2,strlen(buf2),0);

    while(1){

        char buf[100] = {};

        scanf("%s",buf);

        char msg[100] = {};

        sprintf(msg,"%s发送的信息是:%s",name,buf);

        send(clientfd2,msg,strlen(msg),0);

        if (strcmp(buf,"quit") == 0){

            memset(buf2,0,sizeof(buf2));//初始化

            sprintf(buf2,"%s退出了群聊",name);

            send(clientfd2,buf2,strlen(buf2),0);

            break;

        }

    }

    close(clientfd2);

}

void* recv_thread(void* p){

    while(1){

        char buf[100] = {};

        if (recv(clientfd2,buf,sizeof(buf),0) <= 0){

            break;

        }

        printf("%s\n",buf);

    }

}

int main(){

    init();

    printf("请输入用户名:");

    scanf("%s",name);

    printf("\n\n*****************************\n");

    printf("欢迎%s 进入群聊\n",name);

    printf("  输入quit 退出\n");

    printf("\n*****************************\n\n");

    start();

    return 0;

}

  1. 服务器代码(源码)

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <unistd.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <pthread.h>

#include <time.h>

int serverfd;//服务器socket

int clientfd[100];//客户端的socketfd,100个元素,clientfd[0]~clientfd[99]

int size =50;//用来控制进入聊天室的人数为50以内

char* IP = "127.0.0.1";//主机ip地址

short PORT = 6666;//端口号

typedef struct sockaddr meng;

time_t nowtime;

 

 

void init(){

    serverfd = socket(PF_INET,SOCK_STREAM,0);

    if (serverfd == -1){

        perror("创建socket失败");

        exit(-1);

    }

//为套接字设置ip协议 设置端口号  并自动获取本机ip转化为网络ip

    struct sockaddr_in addr;//存储套接字的信息

    addr.sin_family = PF_INET;//地址族

    addr.sin_port = htons(PORT);//设置server端端口号,你可以随便设置,当sin_port = 0时,系统随机选择一个未被使用的端口号

    addr.sin_addr.s_addr = inet_addr(IP);//把127.0.0.1改为自己的server端的ip地址,当sin_addr = INADDR_ANY时,表示从本机的任一网卡接收数据

//绑定套接字

    if (bind(serverfd,(meng*)&addr,sizeof(addr)) == -1){

       perror("绑定失败");

       exit(-1);

    }

    if (listen(serverfd,100) == -1){//监听最大连接数

        perror("设置监听失败");

        exit(-1);

    }

}

 

void SendAll(char* msg){

    int i;

    for (i = 0;i < size;i++){

        if (clientfd[i] != 0){

            printf("发送给%d\n",clientfd[i]);

printf("发送的信息是: %s\n",msg);

//写入文件

char buf[1024];

 

FILE *logs = fopen("log.txt", "a+");

if(logs== NULL)

{

printf("open file erroe: \n");

}else{

sprintf(buf, "进入时间:%s\tIP地址:%s\n",ctime(&nowtime),IP);

fputs(buf,logs);

sprintf(buf, "所发信息:%s\n",msg);

fputs(buf,logs);

fclose(logs);

}

 

            send(clientfd[i],msg,strlen(msg),0);

        }

    }

}

 

void* server_thread(void* p){

    int fd = *(int*)p;

    printf("pthread = %d\n",fd);

    while(1){

        char buf[100] = {};

        if (recv(fd,buf,sizeof(buf),0) <= 0){

            int i;

            for (i = 0;i < size;i++){

                if (fd == clientfd[i]){

                    clientfd[i] = 0;

                    break;

                }

            }

                printf("退出:fd = %d 退出了。\n",fd);

char buf[1024];

FILE *logs = fopen("log.txt", "a");

if(logs== NULL)

{

printf("open file erroe: \n");

}else{

sprintf(buf, "退出时间:%s\tIP地址:%s\n",ctime(&nowtime),IP);

fputs(buf,logs);

fclose(logs);

}

                pthread_exit(0);

        }

        //把服务器接受到的信息发给所有的客户端

        SendAll(buf);

    }

}

void server(){

    printf("服务器启动\n");

    while(1){

        struct sockaddr_in fromaddr;

        socklen_t len = sizeof(fromaddr);

        int fd = accept(serverfd,(meng*)&fromaddr,&len);

//调用accept进入堵塞状态,等待客户端的连接

        if (fd == -1){

            printf("客户端连接出错...\n");

            continue;

        }

        int i = 0;

        for (i = 0;i < size;i++){

            if (clientfd[i] == 0){

                //记录客户端的socket

                clientfd[i] = fd;

                printf("线程号= %d\n",fd);//

                //有客户端连接之后,启动线程给此客户服务

                pthread_t tid;

                pthread_create(&tid,0,server_thread,&fd);

                break;

            }

        if (size == i){

            //发送给客户端说聊天室满了

            char* str = "对不起,聊天室已经满了!";

            send(fd,str,strlen(str),0);

            close(fd);

        }

        }

    }

}

int main(){

    init();

    server();

}

4.实现原理及过程

 

客户端过程:

客户端的过程比较简单,创建 Socket,连接服务器,将 Socket 与远程主机连接(注意:只有 TCP 才有“连接”的概念,一些 Socket 比如 UDP、ICMP 和 ARP 没有“连接”的概念),发送数据,读取响应数据,直到数据交换完毕,关闭连接,结束 TCP 对话。

 

服务端过程:

  1. 咱再来聊聊服务端的过程,服务端先初始化 Socket,建立流式套接字,与本机地址及端口进行绑定,然后通知 TCP,准备好接收连接,调用 accept() 阻塞,等待来自客户端的连接。如果这时客户端与服务器建立了连接,客户端发送数据请求,服务器接收请求并处理请求,然后把响应数据发送给客户端,客户端读取数据,直到数据交换完毕。最后关闭连接,交互结束。
  2. 在服务端启动后,调用 accept() 时,Socket 会进入waiting状态。客户端请求连接时,方法建立连接并返回服务器。accept() 返回一个含有两个元素的元组 (conn, addr)。第一个元素 conn 是新的 Socket 对象,服务器必须通过它与客户端通信;第二个元素 addr 是客户端的 IP 地址及端口。
  3. 接下来是处理阶段,服务器和客户端通过 send()recv() 通信(传输数据)。
    服务器调用 send(),并采用字符串形式向客户端发送信息,send() 返回已发送的字符个数。
    服务器调用 recv() 从客户端接收信息。调用 recv() 时,服务器必须指定一个整数,它对应于可通过本次方法调用来接收的最大数据量。recv() 在接收数据时会进入blocked状态,最后返回一个字符串,用它表示收到的数据。如果发送的数据量超过了 recv() 所允许的,数据会被截短。多余的数据将缓冲于接收端,以后调用 recv() 时,会继续读剩余的字节,如果有多余的数据会从缓冲区删除(以及自上次调用 recv() 以来,客户端可能发送的其它任何数据)。传输结束,服务器调用 Socket 的 close() 关闭连接。
  4. TCP 三次握手的 Socket 过程:

  • 服务器调用 socket()bind()listen() 完成初始化后,调用 accept() 阻塞等待;
  • 客户端 Socket 对象调用 connect() 向服务器发送了一个 SYN 并阻塞;
  • 服务器完成了第一次握手,即发送 SYN 和 ACK 应答;
  • 客户端收到服务端发送的应答之后,从 connect() 返回,再发送一个 ACK 给服务器;
  • 服务器 Socket 对象接收客户端第三次握手 ACK 确认,此时服务端从 accept() 返回,建立连接

  5.TCP 四次挥手的 Socket 过程:

  • 某个应用进程调用 close() 主动关闭,发送一个 FIN;
  • 另一端接收到 FIN 后被动执行关闭,并发送 ACK 确认;
  • 之后被动执行关闭的应用进程调用 close() 关闭 Socket,并也发送一个 FIN;
  • 接收到这个 FIN 的一端向另一端 ACK 确认。

5.运行截图

        

图 1 启动服务端

 

图 2 服务端成功启动

 

                            

图 3 启动客户端

图 4 客户端输入用户名

 

图 5 启动两个客户端

 

图 6 群聊实现

 

注:上面的代码如果直接拷入可能无法使用(字符编码问题)

借鉴(Socket 通信原理):https://segmentfault.com/a/1190000013712747

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐