03. 简易服务器/客户端

# 03. 简易服务器/客户端

上回书说到套接字编程,这章咱接着唠。咱们要写俩简单的程序(不过这俩程序可不太完整,还有点小毛病),用它们来演示上一章提到的那些系统调用。第一个程序是服务器,它能接收客户端的连接,读取一条消息,再回一条消息。第二个程序是客户端,它负责连接服务器,发一条消息,再读一条消息。咱先从服务器这边开始捣鼓。

首先,得搞到一个套接字文件描述符(fd):

int  fd  =  socket(AF_INET,  SOCK_STREAM,  0);
1

这里的AF_INET表示使用IPv4,如果想用IPv6或者双栈套接字,那就得用AF_INET6。为了简单起见,这本书里咱们就只用AF_INET啦。

SOCK_STREAM表示使用TCP协议。在这本书里,咱就只用TCP,别的协议先不考虑。这里socket()调用的三个参数在本书中都是固定的。接下来,再给大家介绍一个新的系统调用:

int val =  1;
setsockopt(fd,  SOL_SOCKET,  SO_REUSEADDR,  &val,  sizeof(val));
1
2

setsockopt()这个调用主要用来配置套接字的各种属性。上面这段代码启用了SO_REUSEADDR这个选项。要是没有这个选项,服务器重启的时候,可能就没办法绑定到同一个地址啦。这里给大家留个小作业:去搞清楚SO_REUSEADDR到底是干啥的,为啥要用它。

接下来就是bind()listen()这俩步骤啦,咱们把服务器绑定到通配地址0.0.0.01234端口上:

//  bind, 这是处理IPv4地址的语法
struct  sockaddr_in addr =  {};
addr.sin_family =  AF_INET;
addr.sin_port =  ntohs(1234);
addr.sin_addr.s_addr =  ntohl(0);       // 通配地址0.0.0.0
int rv =  bind(fd,   (const sockaddr * )&addr,  sizeof(addr));
if  (rv)  {
    die("bind()");
}
1
2
3
4
5
6
7
8
9
//  listen
rv =  listen(fd,  SOMAXCONN);
if  (rv)  {
    die("listen()");
}
1
2
3
4
5

然后进入循环,处理每一个连接:

while  (true)  {
    //  accept
    struct  sockaddr_in client_addr =  {};
    socklen_t socklen =  sizeof(client_addr);
    int connfd =  accept(fd,   (struct  sockaddr * )&client_addr,  &socklen);
    if  (connfd <  0)  {
        continue;      // 出错了,跳过这次循环
    }

    do_something(connfd);
    close(connfd);
}
1
2
3
4
5
6
7
8
9
10
11
12

do_something()函数的功能很简单,就是读取和写入数据:

static void do_something(int connfd)  {
    char rbuf[64]  =  {};
    ssize_t n =  read(connfd,  rbuf,  sizeof(rbuf)  -  1);
    if  (n <  0)  {
        msg("read() error");
        return ;
    }
    printf("client says: %s\n",   rbuf);

    char wbuf[]  =  "world";
    write(connfd,  wbuf,  strlen(wbuf));
}
1
2
3
4
5
6
7
8
9
10
11
12

注意哈,read()write()调用会返回读取或写入的字节数。真正的程序员肯定得处理这些函数的返回值,不过在这章里,为了简洁,我省略了好多东西。而且说实话,这章里的代码在实际的网络编程里,也不是啥“标准答案”。

客户端的程序就简单多啦:

int fd =  socket(AF_INET,  SOCK_STREAM,  0);
if  (fd <  0)  {
    die("socket()");
}

struct  sockaddr_in addr =  {};
addr.sin_family =  AF_INET;
addr.sin_port =  ntohs(1234);
addr.sin_addr.s_addr =  ntohl(INADDR_LOOPBACK);    //  127.0.0.1
int rv =  connect(fd,  (const struct  sockaddr * )&addr,  sizeof(addr));
if  (rv)  {
    die("connect");
}

char msg[]  =  "hello";
write(fd,  msg,  strlen(msg));

char rbuf[64]  =  {};
ssize_t n =  read(fd,  rbuf,  sizeof(rbuf)  -  1);
if  (n <  0)  {
    die("read");
}
printf("server says: %s\n",   rbuf);
close(fd);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

用下面的命令来编译咱们的程序:

g++ -Wall -Wextra -O2 -g 03_server.cpp -o server
g++ -Wall -Wextra -O2 -g 03_client.cpp -o client
1
2

先在一个窗口里运行./server,然后在另一个窗口里运行./client。你应该能看到这样的结果:

$ ./server
client says: hello
1
2
$ ./client
server says: world
1
2

给大家布置个小任务:去看看这章用到的API的手册,或者在网上找些相关教程。一定要学会怎么查找API的使用方法,因为这本书不会详细介绍这些API的具体用法。

  • 03_client.cpp
  • 03_server.cpp
上次更新: 2025/03/25, 00:48:42

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>