Linux网络编程1:C语言服务器端与客户端案例详解

图示流程

1 客户端简单代码

#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#define SERV_PORT 9000    //要连接到的服务器端口,服务器必须在这个端口上listen着
int main(int argc, char *const *argv)
{    
    //这些演示代码的写法都是固定套路,一般都这么写
    int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建客户端socket,地址家族,套接字类型,套接字协议
    struct sockaddr_in serv_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    
    //设置要连接到的服务器的信息
    serv_addr.sin_family = AF_INET;                //选择协议族为IPV4
    serv_addr.sin_port = htons(SERV_PORT);         //连接到的服务器端口,服务器监听这个地址
    //这里为了方便演示,要连接的服务器地址固定写
    if(inet_pton(AF_INET,"192.168.1.126",&serv_addr.sin_addr) <= 0)  //IP地址转换函数,把第二个参数对应的ip地址转换第三个参数里边去,固定写法
    {
        printf("调用inet_pton()失败,退出!\n");
        exit(1);
    }
    //连接到服务器
    if(connect(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0)
    {
        printf("调用connect()失败,退出!\n");
        exit(1);
    }
    int n;
    char recvline[1000 + 1];
    while(( n = read(sockfd,recvline,1000)) > 0) //仅供演示,非商用,所以不检查收到的宽度,实际商业代码,不可以这么写
    {
        recvline[n] = 0; //实际商业代码要判断是否收取完毕等等,所以这个代码只有学习价值,并无商业价值
        printf("收到的内容为:%s\n",recvline);
    }
    close(sockfd); //关闭套接字
    printf("程序执行完毕,退出!\n");
    return 0;
}

1.1 拆分步骤

1.1.1 创建socket

作用为确定连接的类型

int socket(int domain, int type, int protocol);
  • 参数:

1.domain:协议族,协议族决定了socket的地址类型,在通信中必须采用相应的地址。
2.type: 指定socket的数据传输方式(因为domain的协议族也可能有多个传输方式):

  • SOCK_STREAM:基于TCP的流格式套接字,不存在数据边界(发送方发多少次,接收方可以只收一次)
  • SOCK_DGRAM:基于UDP的数据报格式套接字,存在数据边界(发送方发多少次,接收方收多少次)

3.protocol:常见的协议有IPPROTO_TCP、IPPROTO_UDP、 IPPROTO_SCTP、IPPROTO_TIPC他们分别对应这TCP传输协议,UDP传输协议,STCP传输协议,TIPC传输协议。当protocol为0时,会自动选择type类型对应的默认协议。

  • 返回值

成功时返回一个唯一整数【文件描述符】,失败时返回-1

  • 注意事项
    第三个参数大多数时候传0,即保持默认,除非遇到下面的情况:
    同一协议族中存在多个 数据传输方式 相同的协议(即指定了domain和type,仍不能确定某一协议)

  • 例子

// IPv4家族中面向连接的套接字, IPPROTO_TCP可省略填0
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
// IPv4家族中面向消息的套接字,同样可省略
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP) 

1.1.2 创建结构体

用于存放要连接的IP地址端口号

此部分需要注意两种结构体sockaddr和sockaddr_in。直接向sockaddr写入IP和端口信息较为麻烦,而设置sockaddr_in较为方便;但由于后续需要sockaddr,而两种类型是相似的,直接强制类型转换即可。

另外,sockaddr_in是表示IPv4的结构体,但为什么还要地址族sa_family_t。这是为了和sockaddr保持一致,而sockaddr并非只为IPv4设计。

a、结构体细节

//早期的sockaddr ,并非只为IPv4
struct sockaddr
{ 
    sa_family_t sa_family; /* adress family: AF_XXX */ 
    char sa_data[14];/* 14 bytes of protocol */ 
};

//IPv4的sockaddr
struct sockaddr_in{
    sa_family_t    sin_family;                 //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;                   //16位的端口号,以网络字节序保存
    struct            in_addr  sin_addr;     //32位IP地址
    char              sin_zero[8];             //不使用,一般用0填充,目的是与sockaddr结构体保持一致
};

// 其中in_addr
struct in_addr
{
    in_addr_t s_addr; /*32-bit IPV4 address*/
};

b、使用方式

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;                //选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT);         //连接到的服务器端口,服务器监听这个地址
inet_pton(AF_INET,"192.168.1.126",&serv_addr.sin_addr)  // 设置IP地址

c、端口要转化为网络字节顺序

Linux网络编程2: IP地址的字符串 与 端口号的主机字节序 的 网络字节序转换

include <arap/inet.h>
uint16_t htons(uint16_t hostshort);

htons是将 short变量 从主机字节顺序(大小端)转变成网络字节顺序(大端),大端 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
为什么用16位?因为计算机端口数量就是65536个/2^16个

d、字符串ip地址转为网络字节序整数

Linux网络编程2: IP地址的字符串 与 端口号的主机字节序 的 网络字节序转换

static int inet_pton(int af, const char *src,void *dst)

af: address family(协议族)
src:是个指针,指向保存IP地址字符串形式的字符串。
dst:指向存放网络地址的in_addr结构体的首地址

sockaddr_in中保存IP地址信息的成员为32位整数,所以要将字符串形式的ip地址转化为整数数据。
转换方式1、IP字符串 to 网络字节序

in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_network(const char *cp);

1.1.3 连接

connect并不意味着服务器端调用accept,可能会进入等待队列,这个函数的作用仅是通知 Linux 内核,让内核完成 TCP 三次握手连接。
connect会阻塞,直到连接成功/
客户端IP地址和端口在调用connect时分配,无需bind

int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)

1、sockfd,套接字返回的整数
2、sockaddr,connect中要求的参数是sockaddr结构体,我们构建的是sockaddr_id,但两种结构体都是16字节,结构相似,可以进行强制类型转换。
3、sockaddr的长度,sizeof()即可

1.1.4 连接成功读取数据

int read(int handle,void *buf,int len);

1、int handle 为要读取的文件         
2、void *buf  为要将读取的内容保存的缓冲区         
3、int len    读取文件的长度
返回值是实际读取的字节数

2 服务器简单代码

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

#define SERV_PORT 9000  //本服务器要监听的端口号,一般1024以下的端口很多都是属于周知端口,所以我们一般采用1024之后的数字做端口号

int main(int argc, char *const *argv)
{    
    //这些演示代码的写法都是固定套路,一般都这么写

    //服务器的socket套接字【文件描述符】
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);    

    struct sockaddr_in serv_addr;                  //服务器的地址结构体
    memset(&serv_addr,0,sizeof(serv_addr));
    
    //设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
    serv_addr.sin_family = AF_INET;                //选择协议族为IPV4
    serv_addr.sin_port = htons(SERV_PORT);         //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。

    bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//绑定服务器地址结构体
    listen(listenfd, 32);     //参数2表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成,c/s之间进入正常通讯后,请求数-1

    int connfd;
    const char *pcontent = "I sent sth to client!"; //指向常量字符串区的指针
    for(;;)
    {
        //卡在这里,等客户单连接,客户端连入后,该函数走下去【注意这里返回的是一个新的socket——connfd,后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接】        
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);

        //发送数据包给客户端
        write(connfd,pcontent,strlen(pcontent)); //注意第一个参数是accept返回的connfd套接字
        
        //只给客户端发送一个信息,然后直接关闭套接字连接;
        close(connfd); 
    } //end for
    close(listenfd);     //实际本简单范例走不到这里,这句暂时看起来没啥用
    return 0;
}

2.1 拆分步骤

2.1.1 监听socket的创建

int listenfd = socket(AF_INET, SOCK_STREAM, 0);

同样需要协议族、协议类型

2.1.2 地址结构体

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET; //选择协议族为IPV4 
serv_addr.sin_port = htons(SERV_PORT); //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据 
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 一个服务器可能有多块网卡,每个网卡也可能配置多个IP地址,所以用INADDR_ANY,表示服务器的所有可用IP地址,这样监听到9000端口,无论是哪个IP地址都能收到

2.1.3 绑定

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:bind API能够将套接字文件描述符、端口号和ip绑定到一起。因为socket只指明了协议。
  • 返回值:成功时返回0,失败返回-1
  • 参数:
  1. sockfd: 表示socket函数创建的通信文件描述符
  2. addr: struct sockaddr_in或sockaddr的首地址,用于设定要绑定的ip和端口
  3. addrlen: 表示所指定addr结构体的大小

2.1.4 监听

listen()函数不会阻塞,它仅将该套接字和套接字对应的连接队列长度告诉 Linux 内核后结束。当客户端connect,内核自动完成三次握手

#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • 功能:
    将套接字文件描述符从主动(主动向对方发送数据)转为被动文件描述符,然后用于被动监听客户端的连接
  • 参数:
  1. sockfd 表示socket创建的套接字文件描述符
  2. backlog 指定队列的容量用于记录正在连接但是还没有连接完成的客户端,一般设置队列的容量为2,3即可。队列的最大容量需要小于30
  • 返回值:成功返回0,失败返回-1, errno被设置

2.1.5 TCP连接(受理请求)

注意两个套接字,socket生成的套接字是用于bind绑定和listen监听和accept受理请求,而accept生成的套接字用于和客户端交换数据。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 函数功能:
    从established 状态的连接队列头部取出一个已经完成的连接,产生用于数据IO的套接字(返回值),套接字文件描述符默认是阻塞的,即如果没有客户端请求连接的时候,此时accept会阻塞,直到有客户端连接;如果不想套接字文件描述符阻塞,则可以创建套接字 socket函数 时指定type为SOCK_NOBLOCK。
  • 函数返回值:成功返回套接字描述符,失败返回-1
  • 参数
  1. sockfd 表示socket创建的套接字文件描述符
  2. addr: 用于记录发起连接请求的那个客户端的IP端口
  3. addrlen表示第二个参数addr的大小

2.1.6 数据传输

int write(int handle,void *buf,int len);

和read类似。

3 参考资料

https://blog.csdn.net/z_stand/category_9317001.html

posted @   wallnut  阅读(777)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示