TCP協議實現qq群聊
阿新 • • 發佈:2019-02-15
一、注意的問題
- 客戶端的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;
}