【Linux】socket网络编程基础知识

本文介绍了Linux网络编程的基础知识,包括argc!=2的含义、inetpton函数使用、socket编程接口(如socket(), bind(), listen(), accept(), connect(), write(), send(), recv(), close())以及C/S结构实例。涉及网络协议、套接字通信流程和相关技术如IP地址转换、sockaddr数据结构等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

————————————————
版权声明:本文为优快云博主「SogK1997」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/dive668/article/details/119164392
————————————————

本博文是对大作业的预热,此次大作业涉及到Linux课程第十章:网络编程的相关知识,因此着重以老师PPT上的内容为要,进行代码编写。以C/C++为主。

argc !=2是什么意思

What does argc mean? [duplicate]

argc是参数的数量,并且argv是一个字符串数组。
程序本身是第一个参数argv[0],因此argc总是至少为 1。
那么,argc是2当程序使用一个命令行参数运行。如果它不带参数运行,或者超过一个参数,argc != 2则为真,因此将打印使用消息“Usage: display_image ImageToLoadAndDisplay”,告诉用户如何正确运行它。

inet pton函数

inet_pton()和inet_ntop()函数详解

对stdin,stdout 和STDOUT_FILENO,STDIN_FILENO的学习

对stdin,stdout 和STDOUT_FILENO,STDIN_FILENO的学习

基础知识

10.1 网络协议与体系结构

在这里插入图片描述
五层协议体系结构中各层的功能分别如下:

  • 应⽤层为应用进程提供服务,定义了应⽤进程间通信和交互的规则。
  • 传输层为应用进程提供连接服务,实现连接两端进程的会话。
  • 网络层为分组交换网上的不同主机提供通信服务。
  • 数据链路层可简称为链路层,该层将从网络层获取的IP数据报组装成帧,在网络结点之间以帧为单位传输数据。
  • 物理层以比特为单位传输数据,该层定义了与网络相关的硬件的规范,如表示“0”、“1”电压的电压数值、硬件连接方式等。
    协议通常都由如下几个部分组成:
    (1)待交互数据的结构和格式;
    (2)进行交互的方式,包括数据的类型、对数据的处理动作等;
    (3)事件实现顺序的说明。
    在这里插入图片描述
    体系结构中各层的实现建⽴在其下⼀层所提供的服务上,并向其上层提供服务
    在这里插入图片描述

10.2 socket编程基础

socket本意为“插座”,常被称为套接字,当使用socket进行通讯时,进程会先生成一个socket⽂件,之后再通过socket⽂件进行数据传递。
在TCP/IP协议族中,使用IP地址和端口号可以唯一标识网络中的⼀个进程
在这里插入图片描述
socket接口位于应⽤层与TCP/IP协议族之间,是基于软件的抽象层。
在这里插入图片描述
Linux系统中常用的socket网络编程接口有socket()、bind()、listen()、accept()、connect()、send()、recv()、close(),其中

  • connect客户端专用接口,
  • bind()listen()accept() 为服务器端专用接口,
  • socket()close()则由服务器与客户端共用。

10.3 socket编程接口

socket()

socket()函数存在于函数库sys/socket.h中,其声明如下:

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

socket()函数用于创建套接字,也可以说socket()函数用于打开网络通讯端口,该函数类似于文件操作中的open()函数,若调用成功,也返回⼀个文件描述符,之后应用程序可以采用socket通信中的读写函数在网络中收发数据;若调用失败会返回-1,并设置errno。
参数domain用于选择通信时的协议族。常网设置为:

  • AF_INET:⽹络通信
  • AF_UNIX:本地通信
    这些协议族都在头文件sys/socket.h中定义。

参数type用于指定socket的连接类型,其常用取值分别为:

  • SOCK_STREAM:TCP协议
  • SOCK_DGRAM:UDP协议
  • SOCK_RAW:ICMP协议

参数protocol⼀般设置为0,表示使用默认协议

bind()

bind()函数存在于函数库sys/socket.h中,其声明如下:

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

bind()函数的功能为:使服务器端的一个socket⽂件与网络中的⼀个进程进行绑定,因为文件描述符可标识socket⽂件,“主机名+端口号”可标识网络中的唯⼀进程,因此bind()函数实际上是将服务器端的socket⽂件与网络中的进程地址进行绑定。
参数sockfd指代socket文件的文件描述符,⼀般由socket()函数返回;
参数addr指代需要与服务器进行通信进程的
地址
,其本质为struct sockaddr结构体类型的指针struct sockaddr结构体的类型定义如下:

struct sockaddr {
sa_family_t sa_family;
char sa_data[14]; //进程地址
}

addrlen表示参数addr的长度,实质上addr参数可接受多种协议的结构体,⽽这些结构体的长度各不相同,因此需使⽤参数addrlen额外指定结构体长度

定义⼀个struct sockaddr_in类型的结构体可以使用如下方法:

struct sockaddr_in servaddr; //结构体定义
bzero(&servaddr, sizeof(servaddr)); //结构体清零
servaddr.sin_family = AF_INET; //设置地址类型为AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //设置⽹络地址为INADDR_ANY
servaddr.sin_port = htons(85); //设置端⼜号为85

listen()

listen()函数存在于函数库sys/socket.h中,其声明如下:

int listen(int sockfd, int backlog);

listen()函数仍用于服务器端,从字面上看,其功能为使已绑定的socket监听对应客户端进程状态,但实际上,该函数用于设置服务器同时可建立的连接的数量

  • 参数sockfd表示socket⽂件描述符
  • 参数backlog用于设置请求队列的最大长度
  • listen()函数若调用成功则返回0,表示监听成功;否则返回-1,并设置errno

accept()

accept()函数存在于函数库sys/socket.h中,其声明如下:

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

accept()函数在listen()函数之后使用,其功能为阻塞等待客户端的连接请求
当传输层使用TCP协议时,服务器与客户端在创建连接前,会先经过三次握手机制测试连接,三次握手完成后,服务器调用accept()函数处理连接请求,此时若还没有客户端的请求到达,便阻塞等待调用accept()函数的进程,直到接收到客户端发来的请求,且服务器中已创建的连接数未达到backlogaccept()函数才会返回,并传出客户端的地址

connect()

connect()函数存在于函数库sys/socket.h中,其声明如下:

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

connect()函数用于客户端,该函数的功能为向服务器发起连接请求。 connect()函数的参数与bind()函数中参数的形式⼀致,区别在于bind()中的参数为客户端进程地址,而connect的参数为服务器端地址
connect()函数调用成功则返回0,否则返回-1,并设置errno

write()

函数定义:

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

write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内。write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。
write在什么情况下会阻塞?当kernel的该socket的发送缓冲区已满时。
对于每个socket,拥有自己的send bufferreceive buffer。从Linux 2.6开始,两个缓冲区大小都由系统来自动调节(autotuning),但一般在default和max之间浮动。已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。
接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。
返回值:如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。

send()

send()函数函数存在于函数库sys/socket.h中,其声明如下:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);  

该函数用于向处于连接状态的套接字中发送数据
若调用成功都返回0, 否则返回-1,并设置errno

  • 参数sockfd表示接收端的socket⽂件描述符;
  • 参数buf为指向要发送数据的缓冲区指针
  • 参数len表示缓冲区buf中数据的长度
  • 参数flags表示调用的执行方式(阻塞/非阻塞),当flags设置为0时,可使用之前学习的write()函数替代send()函数。

Linux系统中还提供了sendto()函数和sendsg()函数,这两个函数不但能发送数据给已连接的套接字,还可向未连接的套接字发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);  
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);  

sendto()函数中的前4个参数与send()函数的参数相同,之后的参数dest_addraddrlen分别用于设置接收数据进程的地址地址的长度
sendmsg()函数中的第⼆个参数msgstruct msghdr类型的结构体指针,该参数用于传入目标进程的地址地址的长度等信息。

函数send()sendto()sendmsg()调用成功并不表示接收端套接字⼀定会接收到发送的数据

recv()

recv()函数存在于函数库sys/socket.h中,其声明如下:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

该函数用于从已连接的套接字中接收信息,若调用成功则返回读到的字节数,否则返回-1,并设置errno
此外,read()函数、recvfrom()函数和recvmsg()函数也用于接收信息

close()

close()函数用于释放系统分配给套接字的资源,该函数即文件操作 中常用于关闭文件的函数,存在于函数库unistd.h中,其声明如下:

int close(int fd);

close()函数中的参数fd文件描述符,当其用于scoket编程中时,需传⼊socket⽂件描述符。该函数调用成功则返回0,否则返回-1,并设置 errno

10.4 socket通信流程

在这里插入图片描述

10.5 相关的知识

网络序字节

若将数据的高字节保存在内存的低地址,将数据的低字节保存在内存的高地址,这种存放方式称为大端模式;相反就是小端模式

  • 网络数据流:大端模式
  • 计算机:大端/小端模式
    Linux系统中提供了⼀些用于字节序转换的函数,这些函数存在于函 数库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);

IP地址转换函数

标准的IPv4格式的地址,但这种格式是为了方便用户对其进行操作,若要使计算机能够识别,需先将其转换为二进制格式
早期Linux系统中常使⽤以下函数来转换IP地址:

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

但以上函数只能处理IPv4的ip地址,且它们都是不可重⼊函数
如今Linux编程中常⽤inet_pton()inet_ntop()来转换IP地址,这两个函数不但能转换IPv4格式的地址in_addr,还能转换IPv6格式的地址in_addr6,它们存在于函数库arpa/inet.h中,函数定义如下:

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

sockaddr数据结构

IPv4和IPv6的地址格式定义在netinet/in.h中:

  • IPv4地址用结构体sockaddr_in表示,该结构体中包含16位的端口号32位的IP地址
  • IPv6地址用结构体sockaddr_in6表示,该结构体中包含16位的端口号128位的IP地址和⼀些控制字段;
  • Unix Domain Socket的地址格式定义在sys/un.h中,用结构体sock_addr_un表示
    bind()accept()connect()等函数的参数应该设计成void *类型,以便接收各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要进行强制类型转换,例如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

10.6 socket本地进程通信

socket原本是为网络通讯设计的,但后来在socket框架的基础上发展出了⼀ 种IPC(进程通信)机制,即UNIX Domain Socket,专门⽤来实现使⽤socket实现的本地进程通信。
本地通信的流程与使⽤的接口与基于TCP协议的网络通信模型相同,其大致流程如下:
1.调⽤socket()函数通信双方进程创建各⾃的socket⽂件
2.定义并初始化服务器端进程的地址,并使用bind()函数将其与服务器端进程绑定
3.调用listen()函数监听客户端进程请求
4.客户端调用connect()函数,根据已明确的客户端进程地址,向服务器发送请求
5.服务器端调⽤accept()函数,处理客户端进程的请求,若客户端与服务器端进程成功建⽴连接,则双方进程可开始通信;
6.通信双方以数据流的形式通过已创建的连接互相发送和接收数据,进行通信;
7.待通信结束后,通信双方各自调用close()函数关闭连接。

socket网络通信不同的是,在本地通信中用到的套接字的结构体类型为socket sockaddr_un

实例:编写C/S结构的程序,分别创建服务器和客户端,客户端发送字符串,服务器端处理并返回

题目

编写C/S结构的程序,分别创建服务器和客户端
客户端的功能是从终端获取⼀个字符串发送给服务器,然后接收服务器返回的字符串并打印;服务器的功能是从客户端发来的字符,将每个字符转换为⼤写再返回给客户端
要求使⽤TCP协议实现。

参考文档

socket编程在windows和linux下的区别

socket编程在windows和linux下的区别

windows下linux下socket编程区别

windows下linux下socket编程区别

tcpclient_linux代码

/*
 * @Description: 
 * @Author: dive668
 * @Date: 2021-07-28 22:39:28
 * @LastEditTime: 2021-07-29 10:09:54
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 80
#define SERV_PORT 6666
int main(int argv,char *argv[])
{
    struct sockaddr_in servaddr; //定义通信进程的地址
    char buf[MAXLINE]; //定义缓冲区,用于存放数据
    int sockfd,n; //创建两个socket的文件描述符
    char *str; //定义用户字符输入
    if(argc!=2) //如果参数数量不为2,即没有带一个参数运行此程序
    {
        fputs("usage:./client message\n",stderr);
        exit(1);
    }
    str=argv[1]; //第一个字符参数赋值给str
    sockfd=socket(AF_INET,SOCK_STREAM,0);//地址类型为AF_INET,连接为TCP,采用默认协议
    //用于打开网络通讯端口,调用成功则返回一个文件描述符,调用失败则返回-1
    bzero(&servaddr,sizeof(servaddr)); //结构体清零
    servaddr.sin_family=AF_INET; //设置地址类型为AF_INET
    inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
    servaddr.sin_port=htons(SERV_PORT); //设置端口号为宏定义的端口SERV_PORT
    connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    //用于客服端,向服务器端发送请求,类似于bind,调用成功则返回0,否则返回-1
    send(sockfd,str,strlen(str),0);
    //向处于连接状态的套接字中发送数据
    //connfd为接收端的socket⽂件描述符
    //buf为要发送的缓冲区中的数据长度
    //flag表示调用的执行方式,设为0则可用write代替send函数
    n=recv(sockfd,buf,MAXLINE,0);
    //从已连接的套接字中接收信息,若调用成功则返回读到的字节数
    printf("Response from setver:\n");
    write(STDOUT_FILENO,buf,n);
    //write把buf指定的内存写入n个字节,放入到指定的socket文件内
    close(sockfd);
    return 0;
}

tcpserver_linux代码

/*
 * @Description: 
 * @Author: dive668
 * @Date: 2021-07-29 09:22:52
 * @LastEditTime: 2021-07-29 09:49:25
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
''' 
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
'''
#define MAXLINE 80
#define SERV_PROT 6666
int main(void)
{
    struct sockaddr_in servaddr,cliaddr; //定义通信进程的地址
    socklen_t cliaddr_len; //定义addr的长度
    int listenfd,connfd; //创建两个socket的文件描述符
    char buf[MAXLINE]; 
    char str[INET_ADDRSERLEN];
    int i,n;
    listenfd=socket(AF_INET,SOCK_STEAM,0); 
    //用于打开网络通讯端口,调用成功则返回一个文件描述符,调用失败则返回-1
    bzero(&servaddr,sizeof(servaddr)); //结构体清零
    servaddr.sin_family=AF_INET; //设置地址类型为AF_INET
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //设置⽹络地址为INADDR_ANY
    servaddr.sin_port=htons(SERV_PROT); //设置端口号为宏定义的端口6666
    bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); //将socket文件与进程绑定
    listen(listenfd,20); //设置服务器可建立连接的数量为20
    printf("Accepting connections...\n");
    while(1)
    {
        cliaddr_len=sizeof(cliaddr);
        connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); //处理客户端连接请求
        n=recv(connfd,buf,MAXLINE,0); //从已连接的套接字中接收信息,若调用成功则返回读到的字节数
        printf("received from %s at PORT %d\n",inet_stop(AF_INET,&cliaddr.sin_addr,str,sizeof(stderr),ntohs(cliaddr.sin_port));
        for(i=0;i<n;++i)
            buf[i]=toupper(buf[i]); //处理缓冲区的数据
        send(connfd,buf,n,0); 
        //向处于连接状态的套接字中发送数据
        //connfd为接收端的socket⽂件描述符
        //buf为要发送的缓冲区中的数据长度
        //flag表示调用的执行方式,设为0则可用write代替send函数
        close(connfd);//关闭连接
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值