在了解服务端编程的流程之前,我们需要先了解几个函数:
1.创建套接字的函数(关于套接字的介绍在上一节中有了简单的描述,其实就是一个文件描述符,也就是一个整数)

首先我们可以在Linux终端输入:man 7 socket来查看系统对这个函数的描述文档。
图片说明
从以上描述我们可以看到,你要使用这个函数,首先要包含sys/socket.h头文件。
然后可以看到,这个函数有三个参数:
a.第一个socket_family表示地址族,这个我们一般用的就是AF_INET,表示ipv4地址族。
b.第二个参数socket_type,从名字我们就可以看出来,是套接字的类型,上节也提到了套接字主要有两种类型,一种是流格式的套接字(SOCK_STREAM),另一种是数据报格式的套接字(SOCK_DGRAM),分别对应TCP协议和UDP协议。
c.第三个参数协议,IPPROTO_TCP表示使用的是TCP协议

调用这个函数,用一个整型接收他的返回值,我们就得到了一个套接字。

2.bind函数,通过这个函数将套接字与IP和端口等进行绑定。
同样我们可以在终端中键入:man 2 bind命令来查看相关文档
图片说明
这个函数也有三个参数:
a.第一个就是我们通过socket函数得到的套接字,也就是你要绑定的套接字。
b.第二个就是一个sockaddr结构,我们只需要知道里面有几个重要的成员就可以了,比如port addr family 分别表示端口,IP地址,地址族等。
c.第三个参数就是第二个结构的长度。

这个函数的返回值表示这个函数是否执行成功,具体细节可以查看man文档

3.listen函数,这个函数就是用来监听套接字的,监听有没有人来连接你套接字绑定的地址和端口。
同样man listen查看man文档
图片说明
这个函数有两个参数:
a.第一个参数是我们通过socket函数拿到的套接字
b.第二个参数表示能连接到这个套接字的最大数值。可以自己设定

4.accept函数,这个函数用来接收客户端连接请求
man accept查看man文档
图片说明
三个参数:
a.第一个是服务端的套接字
b.第二个要注意,这次是客户端的sockaddr结构,它是一个传出参数,也就是你传一个这种结构的地址进去,函数调用结束之后,这个结构就被赋值了。你就可以拿来用了。
c.第三个参数是第二个参数结构的长度

它的返回值也要注意,它返回的是连接服务器的客户端的套接字,我们可以用一个整型来进行接收,我们之前说过,套接字其实就是一个文件描述符,我们得到了这个文件描述符之后,就可以对这个描述符进行文件操作了,比如给它写几个数据,就相当于给客户端发送了数据。

接下来是服务端的代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
    //创建一个套接字,服务端
    int sock_serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    //AF_INET表示的是ipv4地址族,
    //第二个参数SOCK_STREAM表示的是套接字类型:流格式的套接字,第三个参数表示使用的协议是TCP

    //将套接字和IP、端口等绑定
    struct sockaddr_in sockaddr_serv;
    memset(&sockaddr_serv, 0, sizeof(sockaddr_serv));
    sockaddr_serv.sin_family = AF_INET;
    sockaddr_serv.sin_port = htons(1234);
    sockaddr_serv.sin_addr.s_addr = inet_addr("127.0.0.1");
    //这个函数的第一个参数是套接字(整型),第二个参数是用来初始化端口和IP的结构(注意要进行强制转换),第三参数就是结构的大小了
    bind(sock_serv, (struct sockaddr*)&sockaddr_serv, sizeof(sockaddr_serv));

    //监听套接字,看网络上有多少个主机会来和这个套接字进行连接
    listen(sock_serv, 20); //第二个参数是能连接这个套接字的最大数量

    //接收客户端请求
    struct sockaddr_in sockaddr_client;
    socklen_t socklen_client = sizeof(sockaddr_client);
    int sock_client = accept(sock_serv, (struct sockaddr*)&sockaddr_client, &socklen_client);
    //accept的第三个参数是传出参数,也就是把这个参数传给函数accept函数之后,函数会对这个参数进行赋值,函数调用完之后
    //这个参数就有值了,可供调用者使用。


    //向客户端写数据
    char str[] = "I am a boy!";
    write(sock_client, str, sizeof(str));

    //最后要记得关掉套接字,也就是要关掉文件描述符
    close(sock_serv);
    close(sock_client);

    return 0;
}

上面的代码中,我们给服务端sockaddr结构赋值的时候用的是sockaddr_in,所以在赋完值之后准备传给bind函数时要进行强制转换。这里的服务端IP用的是本地回环测试IP 127.0.0.1 也就是把本机当作服务器。

然后就是客户端:
客户端我们要了解一个函数connect:
从这个函数名我们都可以知道,这是用来连接服务器的函数,我们可以想一想,要连接服务器,我们要如何找到服务器呢,当然是通过IP和端口啦。这个IP和端口也是通过sockaddr结构来给的。
通过man 2 connect查看man文档
图片说明
这个函数也有三个参数:
a.第一个参数是客户端的套接字,客户端也是需要用socket函数创建套接字的,因为服务端调用accept函数得到的是客户端的套接字,如果服务器给客户端写了数据,客户端就可以通过这个套接字来读到。
b.第二个参数是sockaddr结构,注意这个结构是服务端的信息。
c.第三个参数是第二个参数的长度。

下面是客户端的代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
    //客户端也要创建一个套接字
    int socket_client = socket(AF_INET, SOCK_STREAM, 0);

    //设置要连接的服务器的IP 端口 地址族等
    struct sockaddr_in sockaddr_serv;
    memset(&sockaddr_serv, 0, sizeof(sockaddr_serv));
    sockaddr_serv.sin_addr.s_addr = inet_addr("127.0.0.1");
    sockaddr_serv.sin_family = AF_INET;
    sockaddr_serv.sin_port = htons(1234);
    //客户端连接服务器
    connect(socket_client, (struct sockaddr *)&(sockaddr_serv), sizeof(sockaddr_serv));

    //读取服务器传回的数据
    char str[50];
    read(socket_client, str, sizeof(str) - 1);
    //回显到终端输出
    printf("get the data from server: %s\n", str);

    close(socket_client);

    return 0;
}

要实现效果我们是要开两个终端的,因为服务端代码启动之后是会进入阻塞状态的,等待客户端的连接。先启动服务端,再启动客户端。

1.启动服务器
图片说明
我们可以看到,执行服务端程序之后,程序阻塞了。

2.然后启动客户端
图片说明
我们可以看到,服务端给客户端写的数据,客户端读了之后,回显到终端了。