Linux之网络编程(一)

    xiaoxiao2025-07-19  14

    提问:

    什么是网络编程?为什么需要网络编程?c语言提供了哪些结构体和接口?如何使用?TCP和UDP的区别?

    Linux之网络编程(一)(参考代码)

    https://blog.csdn.net/birdunderastarrysky/article/details/90631822

    使用SELECT实现即时聊天小程序

     

     

    目录

    提问:

    基本常识:

    英文缩写与全称:

    Linux相关指令:

    1.什么是网络编程?为什么需要网络编程?

    2.c语言提供了哪些结构体和接口?如何使用?

    通用socket地址结构 struct sockaddr:

    主机(大小端)向网络字节序(大端)的整数转换:

    地址格式转化

    域名和IP地址转化:

    socket编程接口(系统调用):

    UDP接收和发送接口:

    其他:

    3.TCP和UDP的区别?


     

    基本常识:

    协议(protocol):通信双方必须遵循的规矩。OSI是Open System Interconnection的缩写,意为开放式系统互联。由国际标准化组织(ISO)制定。该模型定义了不同计算机互联的标准,是设计和描述计算机网络通信的基本框架。OSI模型把网络通信的工作分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。这是一种事实上被TCP/IP 4层模型淘汰的协议。在当今世界上没有大规模使用。Linux网络编程是通过socket接口来进行的。socket接口是一种特殊的I/O接口,它也是一种文件描述符。它是一种常用的进程间通信机制。不仅可以在本地进程间连接,还可以通过网络在不同机器的进程间进行通信。网络字节序是指大端模式。大端模式是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。

    英文缩写与全称:

    TCP 全称为Transmission Control Protocol  传输控制协议UDP 全称为User Datagram Protocol, 中文名是用户数据报协议IP 全称为Internet Protocol Address  互联网协议地址,又译为网际协议地址port 端口domain 域

    Linux相关指令:

    netstat -an|grep ^tcp  查看网络及端口号并通过管道找出与tco相关的。

    1.什么是网络编程?为什么需要网络编程?

    网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。

    本地进程间通信有很多种,比如传递消息:管道、FIFO、消息队列同步:信号量共享内存

    但是我们使用这些很难去定位网络中某一台主机的某一个进程,这也是我们需要新的方法的原因。网络层的IP地址可以唯一的标识网络中的主机,而传输层的端口号又可以唯一标识一个进程,再加上相应的协议,我们构成了一个三元组(IP地址,协议,端口)来定位网络中某一台主机的某一个进程,因此我们便可以在网络中进行通信了。

    第一个被广泛接受的socket API实现于1983年。实际上这组API已经被移植到了所有UNIX实现以及其他大多数操作系统上了。socket API是在POSIX.1g中进行正式规定,它作为标准草案在经历了10年之后于2000年被正式认可。现在它已经被SUSv3所取代了。

     

    2.c语言提供了哪些结构体和接口?如何使用?

    通用socket地址结构 struct sockaddr:

    struct sockaddr{ unsigned short sa_family; //地址簇 char sa_data[14]; //14字节协议地址,包含socket的IP地址和端口号 }; 由于上面不太方便,一般用下面这个,两个是等价的,使用时可以直接强制类型转换 struct sockaddr_in{ short int sin_family; //地址簇 unsigned short int sin_port; //端口号 struct in_addr sin_addr; //IP地址 unsigned char sin_zero[8]; //填0,保持和struct sockaddr保持一样的小 }; struct in_addr{ unsigned long int s_addr; //32位IP地址,网络字节序 }; sin_family:     AF_INET  ——>    IPV4,        AF_INET6  ——>   IPV6

    主机(大小端)向网络字节序(大端)的整数转换:

    #include<netinet/in.h> uint16_t htons(uint16_t host16bit); uint32_t htonl(uint32_t host32bit); uint16_t ntohs(uint16_t net16bit); uint32_t ntohl(uint32_t net32bit); 成功返回想要转换的字节序,失败返回-1。 其中h表示host,即主机。n表示network,即网络。s表示short,l表示long。htons 就表示 主机字节序 转 网络字节序 , 类型为short类型。依此类推。

    地址格式转化

    #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> int inet_aton(const char* straddr, struct in_addr *addrptr); 将点分十进制的IP地址转换为网络字节序的32位二进制数值。 成功返回1,不成功返回0 。 !!!!!! 参数straddr 存放输入的点分十进制的IP地址字符串。 参数addrptr 传出参数,保存网络字节序的32位二进制数。 char* inet_ntoa(struct in_addr inaddr); 将网络字节序的32位二进制数值转换为点分十进制的IP地址。 成功返回点分十进制的IP地址字符串,不成功返回0 。 !!!!!! in_addr_t inet_addr(const char* straddr); 将点分十进制的IP地址转换为网络字节序的32位二进制数值。 成功返回网络字节序的32位二进制数值,失败返回-1 。 int inet_pton(int family, const char* src, void *dst); 功能与inet_aton类似。 参数family为AF_INET表示IPV4,为AF_INET6表示IPV6 。 const char* inet_ntop(int family, const void* src, char *dst, socklen_t len); 功能与inet_ntoa类似。

    域名和IP地址转化:

    #include<netdb.h> struct hostent{ char* h_name; //正式主机名 char** h_aliases; //主机别名,可以有多个别名 int h_addrtype; //主机地址类型,IPV4为AF_INET int h_length; //主机IP地址字节长度,IPV4为4字节 char** h_addr_list; //主机的IP地址列表,可以有多个IP地址 }; struct hostent* gethostbyname(const char* hostname); 通过域名或主机名得到对应的struct hostent结构体。 struct hostent* gethostbyaddr(const char* addr, size_t len, int family); 通过IP地址得到对应的struct hostent结构体。

    socket编程接口(系统调用):

    TCP服务器端:

    read()/write()也是可以使用的,等价于将flags参数指定为0 。 服务器 socket() -> bind() -> listen() -> accept()(阻塞) -> recv()/send() -> close() 客户端 socket() --------------------------> connect() | -> recv()/send() ->close() int socket(int domain, int type, int protocol); 生成一个套接口描述符。 成功返回套接口描述符,失败返回-1 。 参数domain : AF_INET 允许通过IPV4协议网络连接起来的主机上的应用程序之间通信 AF_INET6 允许通过IPV6协议网络连接起来的主机上的应用程序之间通信 AF_UNIX或AF_LOCAL是同义词,允许同一主机上的应用程序通信。 参数type : SOCK_STREAM TCP协议 SOCK_DGRAM UDP协议 从Linux 内核2.6.27开始,允许两个非标准的标记与socket类型取OR。 SOCK_CLOEXEC 导致内核为新文件描述符启用close-on-exec标记(FD_CLOEXEC)。 SOCK_NONBLOCK 导致内核在底层打开着文件描述符上设置O_NONBLOCK标记。 参数protocol : 指定socket所使用的传输协议编号,通常为0 。

     

    int bind(int sockfd, struct sockaddr* my_addr, int addrlen); 用来绑定一个端口和ip地址,使套接口与指定端口号和IP地址相关联。 成功返回0,失败返回-1 。 参数sockfd:套接字描述符 参数my_addr:使用下面的结构体,然后强制转换成(struct sockaddr*)。 参数addrlen:结构体长度 sizeof(struct sockaddr) 。 struct sockaddr_in{ unsigned short sin_family; //协议IPV4:AF_INET uint16_t sin_port; //使用的端口编号 struct in_addr sin_addr; //IP地址 unsigned char sin_zero[8]; //未使用 }; struct in_addr{ uint32_t s_addr; };

     

    int listen(int sockfd, int backlog); 使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求,如果客户端有连接请求,端口就会接受这个连接。 成功返回0,失败返回-1 参数sockfd: socket描述符 参数backlog: 指定同时能处理的最大连接要求,通常为10或者5。最大可设置为128。

     

    int accept(int scokFd, struct sockaddr* addr, int *addrlen); accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个 新的socket编号来处理,原来的socket还能使用,继续监听其他客户机的连接请求。 成功返回新的socket文件描述符,失败返回-1 参数scokFd:socket文件描述符,监听端口 参数addr:结构体指针变量,系统会把远程主机的信息(地址,端口号)保存到这个指针所指的结构体中。 参数addrlen:为整形指针,表示结构体长度,告诉内核有多少空间用于返回的socket地址。 accept()系统调用会创建一个新的socket,并且正是这个新socket会与执行connect()的对等socket进行连接。accept()调用返回的函数结果是已连接的socket的文件描述符。监听socket(sockFd)会保持打开状态,并且可以被用来接受后续的连接。如果不关心对等的socket的地址,可以将后两位参数设置为NULL和0。(后面某个时刻可使用getpeername()系统调用来获取对端的地址)。 int recv(int sockfd, void* buf, int bufLen, unsigned int flags); 通过套接字接收远端主机传来的数据,并把数据存到由参数buf指向的内存空间。 成功返回实际接收到的字符数,失败返回-1 。如果另一边连接断开,recv返回0 。 参数sockfd:表示socket文件描述符 参数buf:表示缓冲区 参数bufLen:表示缓冲区大小 参数flags:通常为0 注意:另一端写端断开,文件描述符会一直可读,recv会一直读到'\0',返回值为0 。 所以可以通过判断recv返回值确定对端是否断开。 read也是一样的。

     

    int send(int sockfd, void* buf, int bufLen, unsigned int flags); 通过套接字把参数buf指向的内存空间中的信息发送给远端主机。 成功返回实际传送出去的字符数,失败返回-1 。 参数sockfd:表示socket文件描述符 参数buf:表示缓冲区 参数bufLen:表示缓冲区大小 参数flags:通常为0 send一个不是socket的文件描述符,send返回-1,errno=88,描述为send: Socket operation on non-socket.send一个不存在的文件描述符,send返回-1,errno=9,描述为send: Bad file descriptor但是并不会导致程序崩溃。 int close(int fd); 释放不再需要的文件描述符,让数据写回磁盘,并释放该文件所占用的资源。 文件顺利关闭返回0,失败返回-1 。 参数fd:表示文件描述符。 int shutdown(int sockfd, int how); 实现半关闭。

    TCP客户端:

    先通过socket()系统调用得到socket文件描述符,然后执行connect即可。客户端端口号由connet()时操作系统随机分配,并直到进程结束,一直保留。 int connect(int sockfd, struct sockaddr* server_addr, socklen_t addrlen); 用来请求连接远程服务器。将参数sockfd的socket连接到server_addr指定的服务器IP和端口号上。 成功返回0,失败返回-1 。 参数sockfd是socket文件描述符。 参数server_addr是结构体,指定想要连接的地址(不是自己的)。(上面有) 参数addrlen是一个整形数,表示struct sockaddr的大小。

    UDP接收和发送接口:

    也需要使用socket()系统调用创建socket文件描述符以及bind()系统调用进行绑定地址和端口号。socket()系统调用等价于建立一个邮箱,(整个运作过程有点像邮政系统)此外,UDP也可以使用connect()系统调用,这会导致内核记录这个socket的对等socket的地址。这也称为已连接的数据报socket。当一个数据报socket已连接后:数据报的发送可以使用write()或send()来完成,并且会自动被发送到同样的对等的socket上。与sendto()一样,每个write()调用会发送一个独立的数据报。在这个socket上只能读取由对等socket发送的数据报。通过再发一起一个connect()系统调用可以修改一个已连接的数据报socket的对等socket 。解除:通过指定一个地址簇(如UNIX domain中的sum_family字段)为AF_UNSPEC的地址结构还可以解除对等关联关系。但是要注意其他很多UNIX实现不支持将AF_UNSPEC用于这种用途。客户端端口号在第一次sendto()或connect()时由操作系统随机分配,然后一直保持,直到进程结束。 服务器端 socket() -> bind() -> recvfrom()/sendto() -> close() 客户端 socket() -----------> sendto()/recvfrom() -> close()

     

    int sockFd = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in serAddr; serAddr.sin_family = AF_INET; serAddr.sin_port=htons(atoi("8888")); serAddr.sin_addr.s_addr=inet_addr("192.168.1.111"); int ret;//接收返回值 ret = bind(sockFd, (struct sockaddr*)&serAddr, sizeof(struct sockaddr)); if(-1 == ret){ perror("bind"); return 0; } struct sockaddr_in clientAddr; //接收对端地址和端口号 bzero(&clientAddr,sizeof(clientAddr)); socklen_t len=sizeof(clientAddr); char buf[128]={0}; recvfrom(socketFd,buf,sizeof(buf),0,(struct sockaddr*)&clientAddr,&len);//接收对端信息

     

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 通过套接字接收远端主机传来的数据,并把数据存到由参数buf指向的内存空间。 成功返回实际接收到的字符数,失败返回-1 。如果另一边连接断开,recv返回0 。 参数sockfd:表示socket文件描述符 参数buf:表示缓冲区 参数bufLen:表示缓冲区大小 参数flags:通常为0 参数src_addr:存放源主机的IP地址和端口号,不需要可以为NULL。 参数addrlen: src_addr的长度,不需要可以为NULL。 不管len参数是多大,recvfrom()只会从一个数据报socket中读取信息。如果消息大小超过了length字节,那么消息会被静默地截断为len字节。

     

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 通过套接字把参数buf指向的内存空间中的信息发送给远端主机。 成功返回实际传送出去的字符数,失败返回-1 。 参数sockfd:表示socket文件描述符 参数buf:表示缓冲区 参数bufLen:表示缓冲区大小 参数flags:通常为0 参数dest_addr和参数addrlen: 如果sendto用于面向连接的模式,这两个参数会被忽视。 如果用于UDP模式,两个参数指定了数据报发送到的socket。(也就是对端,不是自己)

     

    其他:

    //通过该函数打开文件状态标记可以执行非阻塞I/O void setNonBlock(int fd) { int status=fcntl(fd,F_GETFL); status=status|O_NONBLOCK; fcntl(fd,F_SETFL,status); } //通过该函数打开文件状态标记可以执行阻塞I/O void setBlock(int fd) { int status=fcntl(fd,F_GETFL); status=status&~O_NONBLOCK; fcntl(fd,F_SETFL,status); }

     

    3.TCP和UDP的区别?

     

    TCP:也叫流socket,提供了一个可靠的双向的字节流通信信道

    可靠传送不保留消息边界,字节流面向连接

     

     

    UDP:也叫数据报socket,允许数据以数据报道的消息的形式进行交换。

    不可靠传送,无序的,重复的,或者无法到达。保留消息边界不面向连接

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    最新回复(0)