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;
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • 网络编程 一.楔子 你现在已经学会了写python代码,假如你写了两个python文件a.py和b.py,分别去运...
    go以恒阅读 1,971评论 0 6
  • 计算机网络概述 网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输。 按照计算机网络的定义,通过一定...
    蛋炒饭_By阅读 1,203评论 0 10
  • 第一章 引言和网络编程基础知识 1.1 分别简述OSI参考模型和TCP/IP模型,并阐述他们之间的对应关系 1.2...
    V0W阅读 5,289评论 0 9
  • 正坐在机场漫长的等待,想到近几日盘旋在自己脑海中的一个问题,想要记录下来不然又要溜走了 同化?前几天,极其优秀的Y...
    狸多变阅读 336评论 0 0
  • 凌晨一点起来给已经上火得厉害的贝贝倒一杯白开水,就再也睡不着了。耳边是贝贝用力的呼吸,时不时一两声咳嗽。窗外是奔驰...
    笑笑竹笋妹阅读 243评论 0 2