基于TCP通信的简单服务端和客户端程序

背景

因为最近在研究网络相关的东西,因此经常要写程序做实验来验证。主要是TCP通信,因此就写了个简单的基于TCP通信的小程序,方便以后要使用的时候能直接复用,省的还要各种谷歌、百度。

功能介绍

写的很简单,实现的就是客户端读取键盘输入,发送给服务端,服务端打印出该输入。

因为只研究TCP通信原理,就没有再做其他的多线程并发之类的功能。

代码

1、先看服务端代码

/*服务端TCP程序一般流程

*1、创建socket

*2、绑定端口和ip

*3、监听socket

*4、接收客户端的请求

*5、从缓冲区中读取数据

*/

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/un.h>

#include <arpa/inet.h>

#include <netinet/in.h>

#define PORT 5000

#define IP "192.168.0.106"

#define BACKLOG 5

int main()

{

    int listen_fd;

    int accept_fd;

    char buf[1024] = {0};//读写缓冲区

    struct sockaddr_in server_addr;

    //需要获取客户端相关信息

    struct sockaddr_in client_addr;

    socklen_t client_len;

    client_len = sizeof(client_addr);

    //创建socket

    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

        perror("socket");

        return 1;

    }

    //绑定端口和ip

    server_addr.sin_family = AF_INET;

    server_addr.sin_port = htons(PORT);

    server_addr.sin_addr.s_addr = inet_addr(IP);

    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {

        perror("bind");

        return 2;

    }

    //监听socket

    if (listen(listen_fd, BACKLOG) < 0) {

        perror("listen");

        return 3;

    }

    while(1) {

        //接收客户端的请求

        if ((accept_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len)) < 0) {

            perror("accept");

            return 4;

        }

        //打印客户端信息

        printf("connected with ip: %s: port:%d\n",

            inet_ntop(AF_INET, &client_addr.sin_addr, buf, 1024), ntohs(client_addr.sin_port));

        while (1) {

            memset(buf, 0, sizeof(buf));

            //从缓冲区中读取数据

            ssize_t size = read(accept_fd, buf, sizeof(buf) - 1);

            if (size > 0)

                printf("client send: %s\n", buf);

            else if (size == 0) {

                printf("read done!\n");

                close(accept_fd);

                break;

            }

            else {

                perror("read");

                close(accept_fd);

                break;

            } 

        }

    }

    close(listen_fd);

    return 0;

}

2、然后是客户端代码

/*客户端TCP程序一般流程

*1、创建socket

*2、向服务端发起连接

*3、向缓冲区中写入数据

*/

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/un.h>

#include <arpa/inet.h>

#include <netinet/in.h>

#define PORT 5000

#define IP "192.168.0.106"

int main()

{

    int client_fd;

    char buf[1024] = {0};//读写缓冲区

    struct sockaddr_in client_addr;

    ssize_t size;

    //创建socket

    if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

        perror("socket");

        return 1;

    }

    //填充ip端口信息

    client_addr.sin_family = AF_INET;

    client_addr.sin_port = htons(PORT);

    client_addr.sin_addr.s_addr = inet_addr(IP);

    //向服务端发起连接

    if (connect(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) {

        perror("connect");

        return 2;

    }

    while(1) {

        memset(buf, 0, sizeof(buf));

        //从标准输入获取字符串

        if (!gets(buf)) {

            perror("gets");

            return 3;

        }

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

            break;

        if (write(client_fd, buf, strlen(buf)) != strlen(buf)) {

            perror("write");

            return 4;

        }

    }

    close(client_fd);

    return 0;

}

3、最后是一个简单的Makefile

all:

    gcc server.c -o server

    gcc client.c -o client

clean:

    rm -rf server client

编译后程序就能跑起来了,使用netstat命令查看连接状态如下:

客户端退出后,服务端还是在监听新连接的到来。

探讨

1、对于服务端代码的accept函数

一般情况我们是将第二、三个入参置为空的,表示我们不关注客户端信息,如下

accept_fd = accept(listen_fd, NULL, NULL);

此处我们为了打印客户端ip和端口,因此传入了我们定义的结构体,用于获取客户端信息。

2、关于服务端监听ip,或者是说绑定的ip

一般我们服务端程序监听全网段ip,即在调用bind函数时ip地址使用INADDR_ANY作为参数,如下,

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

这两种使用的区别我们可以通过netstat命令查看,对比如下,

上面的图为设置了具体监听ip的程序,下面是使用INADDR_ANY为参数的程序。由此,我们编程时就需要根据环境上是否有多个ip,程序是否需要监听所有网卡的端口来确定我们函数的参数设置。

3、客户端程序一般不调用bind函数

在客户端程序中我们一般不调用bind函数,因为我们其实一般并不关心客户端使用什么ip和什么端口和服务端通信,内核会替你选择一个未被占用的端口以及能和服务端通信的ip来发起连接。但是作为客户端,我们是否可以使用bind函数呢?答案是肯定的。考虑以下场景,如果我们想要指定客户端连接从哪个ip出去,使用哪个端口,这时候就必须使用bind函数了,这也就是bind函数的作用。

我们将上面客户端的程序稍微修改一下,增加bind操作。为了更好的看出bind的结果,我在环境上配了两个ip,让客户端和服务端各在一个ip上进行通信。我们配置客户端使用5555号端口,绑定在ip:192.168.0.107上,如代码所示:

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/un.h>

#include <arpa/inet.h>

#include <netinet/in.h>

#define PORT 5000

#define IP "192.168.0.106"

int main()

{

    int client_fd;

    char buf[1024] = {0};//读写缓冲区

    struct sockaddr_in client_addr;

    ssize_t size;

    //创建socket

    if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

        perror("socket");

        return 1;

    }

    /**************************************************************************/

    //客户端使用bind函数绑定ip和端口

    struct sockaddr_in client_bind;

    client_bind.sin_family = AF_INET;

    client_bind.sin_port = htons(5555);

    client_bind.sin_addr.s_addr = inet_addr("192.168.0.107");

    if (bind(client_fd, (struct sockaddr*)&client_bind, sizeof(client_bind)) < 0) {

            perror("bind");

            return 5;

    }

    /**************************************************************************/

    //填充ip端口信息

    client_addr.sin_family = AF_INET;

    client_addr.sin_port = htons(PORT);

    client_addr.sin_addr.s_addr = inet_addr(IP);

    //向服务端发起连接

    if (connect(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) {

        perror("connect");

        return 2;

    }

    while(1) {

        memset(buf, 0, sizeof(buf));

        //从标准输入获取字符串

        if (!gets(buf)) {

            perror("gets");

            return 3;

        }

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

            break;

        if (write(client_fd, buf, strlen(buf)) != strlen(buf)) {

            perror("write");

            return 4;

        }

    }

    close(client_fd);

    return 0;

}

编译运行后同样使用netstat命令观察连接状态,如下:

对比之前的截图我们可以发现,客户端的ip和端口已经变成我们设置的值。所以说,bind在客户端也是可以使用的。

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

推荐阅读更多精彩内容