1. 程式人生 > >TCP協議實現qq群聊

TCP協議實現qq群聊

一、注意的問題

  • 客戶端的0號檔案描述要設定成非阻塞,這樣才能接受其他客戶端發的訊息。
  • 客戶端的讀寫操作也必須設定成非阻塞。
  • 對服務端使用setsockopt函式,允許建立多個埠號相同的套接字(解決伺服器先關閉而引發TIME_WAIT狀態的一系列問題)

二、原始碼

comm.h

#ifndef __COMM_H__
#define __COMM_H__

#define NAMESIZE 16
#define DATASIZE 1024

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h> #include <netinet/in.h> #include <pthread.h> #include <sys/socket.h> #include <assert.h> #include <arpa/inet.h> #include <fcntl.h> #include <signal.h> typedef struct Msg { char name[NAMESIZE]; char data[DATASIZE]; }Msg;

server.c

#include "comm.h"

typedef struct sockfd_node{
    int fd;
    struct sockfd_node* _prev;
    struct sockfd_node* _next;
}sockfd_node;

sockfd_node* phead = NULL;

sockfd_node* buy_node(int client_fd)
{
    sockfd_node* new_node = (sockfd_node*)malloc(sizeof(sockfd_node));
    assert(new_node != NULL);

    new_node->fd = client_fd;
    new_node->_prev = NULL;
    new_node->_next = NULL;
    return
new_node; } void push_back(sockfd_node* new_node) { if(phead->_next == NULL){ phead->_next = new_node; phead->_prev = new_node; new_node->_next = phead; new_node->_prev = phead; }else{ new_node->_prev = phead->_prev; new_node->_next = phead; phead->_prev->_next = new_node; phead->_prev = new_node; } } // 刪除節點 void erase(sockfd_node* del) { del->_prev->_next = del->_next; del->_next->_prev = del->_prev; free(del); } // 接收到 Ctrl+C 產生的 資訊後,執行此函式 void handler(int n) { sockfd_node* cur = phead->_next; while(cur != phead){ sockfd_node* next = cur->_next; free(cur); cur = next; } free(phead); printf("伺服器退出\n"); exit(0); } void* thread_work(void* attr) { sockfd_node* cur = (sockfd_node*)attr; // 儲存客戶端發來的訊息以及客戶端的姓名 Msg msg; memset(&msg, 0, sizeof(msg)); for(;;){ // 讀取客戶端資訊 int read_size = recv(cur->fd, &msg, sizeof(msg), 0); // 讀到的位元組個數為0,代表客戶端斷開連線 if(read_size == 0){ printf("< < < < < %s 已下線 < < < < <\n", msg.name); erase(cur); break; }else{ if(read_size < 0){ perror("recv"); break; } } printf("%s say : %s\n", msg.name, msg.data); // 把收到訊息給所有使用者傳送 sockfd_node* p = phead->_next; while(p != phead){ if(p != cur){ send(p->fd, &msg, sizeof(msg), 0); } p = p->_next; } } } void server_work(int argc, char* argv[]) { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if(server_fd < 0){ perror("socket"); } // 允許建立多個埠號相同的套接字 // 解決:如果伺服器主動斷開連線會進入 TIME_WAIT 狀態的問題 int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in server_socket; server_socket.sin_family = AF_INET; server_socket.sin_addr.s_addr = inet_addr(argv[1]); server_socket.sin_port = htons(atoi(argv[2])); int ret = bind(server_fd, (struct sockaddr*)&server_socket, sizeof(server_socket)); if(ret < 0){ perror("bind()"); close(server_fd); } if(listen(server_fd, 5) < 0){ perror("listen"); close(server_fd); } printf("bind and listen success, wait accept...\n"); for(;;){ struct sockaddr_in client_socket; socklen_t len = sizeof(client_socket); // 與客戶端建立連線 int client_fd = accept(server_fd, (struct sockaddr*)&client_socket, &len); if(client_fd < 0){ continue; } char* ip = inet_ntoa(client_socket.sin_addr); int port = ntohs(client_socket.sin_port); printf("get accept, ip : %s, port : %d\n", ip, port); // 把連線的客戶端的資訊存放在連結串列裡,統一管理 sockfd_node* new_node = buy_node(client_fd); push_back(new_node); pthread_t tid = 0; pthread_create(&tid, NULL, thread_work, (void*)new_node); // 把執行緒設定成分離狀態,使其結束後被作業系統自動回收(不會阻塞主執行緒) pthread_detach(tid); } close(server_fd); } int main(int argc, char* argv[]) { // 把伺服器設定成守護程序 //daemon(0, 0); phead = buy_node(0); if(argc != 3){ printf("Usage ./server [ip] [port]\n"); return 1; } // 當按下 Ctrl+C 時,先銷燬連結串列釋放空間,然後伺服器退出 signal(SIGINT, handler); server_work(argc, argv); return 0; }

client.c

#include "comm.h"


int client_work(int argc, char* argv[])
{
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_fd < 0){
        perror("socket");
        return 2;
    }

    struct sockaddr_in server_socket;
    //struct sockaddr_in client_socket;
    server_socket.sin_family = AF_INET;
    server_socket.sin_addr.s_addr = inet_addr(argv[1]);
    server_socket.sin_port = htons(atoi(argv[2]));

    int ret = connect(server_fd, (struct sockaddr*)&server_socket, sizeof(server_socket));
    if(ret < 0){
        perror("connect");
        return 3;
    }

    printf("%s connect success...\n", argv[3]);

    if(fcntl(0, F_SETFL, FNDELAY) < 0){
        perror("fcntl");
        return 4;
    }

    Msg msg;
    for(;;){
        memset(&msg, 0, sizeof(msg));
        strcpy(msg.name, argv[3]);
        int read_size = read(0, msg.data, sizeof(msg.data));
        if(read_size > 0){
            msg.data[read_size-1] = '\0';
            send(server_fd, &msg, sizeof(msg), MSG_DONTWAIT);
        }

        int recv_size = recv(server_fd, &msg, sizeof(msg), MSG_DONTWAIT);
        if(recv_size > 0){
            printf("\t\t%s say : %s\n", msg.name, msg.data);
        }
    }
}

int main(int argc, char* argv[])
{
    if(argc != 4){
        printf("Usage ./server [ip] [port] [name]\n");
        return 1;
    }
    client_work(argc, argv);

    return 0;
}