Linux网络编程(TCP套接字)

一、socket套接字

如果学习或者了解过计算机网络就会知道分层的概念,一台计算机的数据通过层层包装之后,发送到网络上,另一台计算机将这些数据层层剥离得到真实的数据。这里简单提一下。可以参考sokcet编程http://c.biancheng.net/view/2126.html。

参考模型

上面提到的封装思想,是通过一些参考模型来实现的。两个常见的模型:OSI参考模型、TCP/IP模型。

在这里插入图片描述

OSI是最初为了实现计算机网络通信提出的概念模型包含7层,非常复杂难以实现,真正实现网络通信之后使用的是TCP/IP模型。在TCP/IP模型中,分为四层:网络接口层、网络层、传输层、应用层。我们使用的socket套接字在传输层,因此包含:TCP套接字UDP套接字

二、socket通信

1、通信流程

C/S模型下使用socket套接字进行网络通信的流程如图:

在这里插入图片描述

2、函数说明

上面的流程列出了具体使用到的函数,下面说明一下这些函数:

2.1socket接口

socket是插座、接口的意思,在进行通信之前,我们需要通过socket来创建一个接口,这样才可以进行后续的通信。

函数原型:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明:

  • domain:旧版本中这个参数名为af,af也就是地址族(address family),说明这是一个IP地址。IP地址包含IPv4和 IPv6,使用前者为AF_INET,后者则为AF_INET6。

  • type:这个参数是数据传输方式/套接字类型,常用的有流式套接字(SOCK_STREAM)和数据报套接字 (SOCK_DGRAM),流式套接字使用面向连接的TCP传输协议,数据报套接字使用无连接的UDP传输协议。

  • protocol:协议,设置套接字使用的协议,常用的就是前面说的TCP和UDP协议,对应名分别为IPPROTO_TCP 、IPPROTO_UDP。

  • 返回值:socket文件描述符。

有的时候确定了前面两个参数也可以不使用后面的protocol(协议),设置为0即可。系统会推出使用什么协议进行通信,如:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字
2.2 bind绑定

socket通信时,server服务器端绑定客户端,然后对其进行监听。

函数原型:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:socket文件描述符。

  • sockaddr:sockaddr结构体指针,sockaddr结构体原型如下:

    struct sockaddr {
        sa_family_t sa_family;
        char        sa_data[14];
    }
    

    成员:sa_family说明地址类型,IPv4或IPv6,参考上一个函数。

    sa_data[14]用来保存IP地址和端口号。

    注意:一般使用sockaddr_in结构体填充地址和端口,最后强制转换为sockaddr类型填入bind函数。

  • addrlen:前一个参数的大小一般使用sizeof(addr)。

  • 返回值:成功返回0,失败返回-1,设置errno相应错误。

这里简单说明一下为什么使用sockaddr_in而不是使用sockaddr结构体填充ip地址和端口号,sockaddr_in结构体类型如下:

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

比较一下就可以看出它们的区别,sockadd_in结构体包含成员sin_port和sin_addr,表示端口和ip地址,使用时直接向里面填充即可。sin_zero[8]一般不使用。

端口sin_port是16位的数据类型,端口范围为0 ~ 65535,熟知端口范围是0~1023。因为最终需要将sockaddr_in结构体转换为sockaddr结构体成员的char类型,所以端口号需要进行转换,例如: htons(1234);

ip地址使用的是点分十进制字符串,也需要通过函数进行转换,例如:inet_addr("127.0.0.1")

简单bind()绑定示例:

//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
serv_addr.sin_family = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
serv_addr.sin_port = htons(1234);  //端口

//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
2.3 connect连接

当client客户端对服务器发起连接请求时,需要使用connect()连接服务器。

函数原型:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

参数类型与上一个函数相同,参考上一个函数。但需要注意的是,connect()需要设置的是服务器的地址和端口。

2.4 listen监听

当服务器和客户端建立连接后,服务器需要监听客户端的读写请求,需要使用listen()函数对客户端进行监听。

函数原型:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

参数说明:

  • sockfd:socket文件描述符。

  • backlog:定义的请求队列的最大长度。

  • 返回值:成功返回0,失败返回-1,设置errno相应错误。

2.5 accept接收

当监听到请求时,可以通过accpet函数接收请求。

函数原型:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数类型说明参考connect函数和bind函数,注意最后一个参数为指针类型。

注意:accpet()接收请求成功之后,返回的是一个新的套接字,使用新的套接字和客户端进行通信,参数addr中会保存当前通信的客户端的地址和端口信息。

2.6 write写入

linux下一切皆文件,所以当我们使用socket通信需要发送数据时,使用write函数。这里的write函数与以前的文件操作的write没有区别,read也是同理。

函数原型:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:使用socket使用这个参数即为sockfd。
  • buf:需要写入的数据,为空指针类型。
  • conut:写入数据的大小。
  • 返回值:写入的字节数。
2.7 read读取

接收数据使用read函数。

函数原型:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:使用socket使用这个参数即为sockfd。
  • buf:读出的数据后存放的位置。
  • conut:读取数据的大小。
  • 返回值:读出的字节数。

2.8 转换函数

函数原型:

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

函数描述:

htonl函数的作用是:将无符号整型hostlong主机从主机字节顺序转换为网络字节顺序。

htons函数的作用是:将无符号短整型hostshort主机从主机字节顺序转换为网络字节顺序。

ntohl函数的作用是:将无符号整数netlong从网络字节顺序转换为主机字节顺序。

ntohs函数的作用是:将无符号短整数netshort从网络字节顺序转换为主机字节顺序。

当使用端口和ip地址时需要使用这些函数转换为特定的格式。

三、通信实例

服务器端

server.c:

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


int main(int argc, char *argv[])
{
    int fdServ, fdClnt;
    char bufServ[]  = "I'm a server!";
    char sBuf[40] = {0};
    struct sockaddr_in servAddr, clntAddr;
    memset(&servAddr, '\0', sizeof(servAddr));

    servAddr.sin_family = AF_INET;//ipv4
    servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址为本机ip
    servAddr.sin_port = htons(1234);//端口1234
    //创建流式套接字
    fdServ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//ipv4 流式套接字 TCP协议

    if(bind(fdServ, (struct sockaddr *)&servAddr, sizeof(struct sockaddr)) == 0)
    {   //绑定客户端成功
        printf("bind  success!\n");
        //监听客户端请求
        if(listen(fdServ, 20) == 0)
            printf("Server listening...\n");
        //阻塞等待请求
        socklen_t clntAddr_size = sizeof(clntAddr);
        fdClnt = accept(fdServ, (struct sockaddr *)&clntAddr, &clntAddr_size);

        if(write(fdClnt, bufServ, sizeof(bufServ)) > 0)
        {   //写入数据成功
            printf("Server write success!\n");
        }
        else{
            printf("Write failed!\n");
            perror("Server write");
            exit(1);
        }
        if(read(fdClnt, sBuf, sizeof(sBuf)) > 0)
        {   //读取成功
            printf("Server read data success!\n");
            printf("-----------------Data from client: %s\n", sBuf);
        }
    }

    close(fdClnt);
    close(fdServ);
    return 0;
}
客户端

client.c:

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


int main(int argc, char *argv[])
{
    int fdServ;
    char bufRead[40]={0};
    char cBuf[] = "I'm a client!";
    struct sockaddr_in servAddr;
    memset(&servAddr, '\0', sizeof(servAddr));
    //创建socket套接字
    fdServ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servAddr.sin_port = htons(1234);

    if(connect(fdServ, (struct sockaddr *)&servAddr, sizeof(servAddr)) == 0)
    {
        printf("Connect to server success!\n");
        if(read(fdServ, bufRead, sizeof(bufRead)) > 0)
        {   //读取到数据
            printf("-----------------Data from server: %s\n", bufRead);
        }
        // sleep(10);
        printf("cBuf = %s\n", cBuf);
        if(write(fdServ, cBuf, sizeof(cBuf)) > 0)
        {   //写入数据成功
            printf("client write data success!\n");
        }
    }
    
    close(fdServ);
    return 0;
}
运行验证
在这里插入图片描述
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值