网络编程之套接字socket

本文详细介绍了网络编程中的套接字(socket)概念,包括C/S模式和B/S模式的区别,以及socket编程的核心流程。重点讲解了socket()、bind()、listen()、accept()和connect()等关键函数的用途和参数,阐述了服务器和客户端如何通过socket进行网络通信。

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

我们知道网络层提供ip地址的概念来标识不同网络中的主机,而且传输层提供端口的概念来标识主机中的进程(这个端口和路由器中的端口概念不同,路由器的端口用来标识下一个转发)通过【ip,端口号】的组合方式,按理说就可以进行网络的进程通信。

网络通信

现在网络编程无非C/S模式和B/S模式,C/S模式即为客户端/服务器,是最经典的模式,QQ、网易云,你下载了他们家得软件,就是他们家得客户了,打开软件,你就是客户端了,在软件上和他们家得服务器建立连接,这样就达到了通信的目的。C/S结构在技术上很成熟,它的主要特点是交互性强、具有安全的存取模式、网络通信量低、响应速度快、利于处理大量数据。但是该结构的程序是针对性开发,变更不够灵活,维护和管理的难度较大。并且,由于该结构的每台客户机都需要安装相应的客户端程序,分布功能弱且兼容性差,不能实现快速部署安装和配置,因此缺少通用性,具有较大的局限性。要求具有一定专业水准的技术人员去完成。

B/S模式即为浏览器/服务器,通过打开浏览器和输入服务器的域名就可以建立连接,现在很多APP都支持浏览器网页访问。B/S结构应用程序相对于传统的C/S结构应用程序是一个非常大的进步。 B/S结构的主要特点是分布性强、维护方便、开发简单且共享性强、总体拥有成本低。但数据安全性问题、对服务器要求过高、数据传输速度慢、软件的个性化特点明显降低,这些缺点是有目共睹的,难以实现传统模式下的特殊功能要求。例如通过浏览器进行大量的数据输入或进行报表的应答、专用性打印输出都比较困难和不便。此外,实现复杂的应用构造有较大的困难。

socket 编程

计算机有五层网络协议栈,数据经过一层层的封装解封装,最终到达目的地,在应用层和网络层的中间层,就是socket提供得软件抽象层,提供很多API供用户层使用。网络中进程通信无处不在,几乎所有的应用程序都是采用socket,所以说socket的学习是网络编程必不可少的过程。

那么在linux下,一切皆文件,所有的输入输出操作都是对文件的操作,socket会创建一个类似于文件描述符的描述符socket fd,然后对socket描述符进行操作,这些操作都是APUE里面讲得,做网络编程必须要懂。

首先先不讲数据操作的API,先讲讲建立连接的流程,和一些建立连接的API,因为作为网络进程,所有的一切通信都是建立在socket的基础上的,基本的流程都是差不多的,首先是服务器建立连接的流程。

在这里插入图片描述

server

服务器一般来说是被动的一方,得先做好被连接的准备

socket()

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

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数
分别为:

  • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定socket类型。常用的socket类型有,SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
  • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

bind()

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

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数。通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。

函数的三个参数:

  • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
  • addrlen:对应的是地址的长度。
  • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核:

ipv4对应的是sockaddr_in类型定义:

struct sockaddr_in {
	sa_family_t sin_family;
	in_port_t sin_port;//端口
	struct in_addr sin_addr;
};
struct in_addr {
	uint32_t s_addr;//32位ip地址
};

ipv6对应的是sockaddr_in6类型定义:

struct sockaddr_in6 {
	sa_family_t sin6_family;
	in_port_t sin6_port;//端口
	uint32_t sin6_flowinfo;
	struct in6_addr sin6_addr;
	uint32_t sin6_scope_id;
};
struct in6_addr {
	unsigned char s6_addr[16];//ipv6类型的地址
};

Unix域对应的sockaddr_un类型的定义:

#define UNIX_PATH_MAX 108
struct sockaddr_un {
	sa_family_t sun_family;
	char sun_path[UNIX_PATH_MAX];
};

listen()
socket()函数创建的socket默认是一个主动类型的,如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,该函数将socket变为被动类型的,等待客户的连接请求

int listen(int sockfd, int backlog);
  • sockefd: socket()系统调用创建的要监听的socket描述字
  • backlog: 相应socket可以在内核里排队的最大连接个数

accept()
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。服务器之后就会调用accpet()接受来自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止。一旦客户端调用connect()函数就会触发服务器的accept()返回,这时整个TCP链接就建立好了。

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

参数同bind()。

accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。

close(int fd)
关闭描述符

client

客户端也需要通过socket()创建socket描述符

connect()

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

TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服务器。如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接,通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。

参数同bind()。

I/O操作

常常建立网络链接后会搭配一些读写操作

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

read()是负责从fd中读取内容。当读成功时read返回实际所读的字节数;如果返回的值是0表示已经读到文件的结束了,如果是网络socke fd也就意味着TCP 链接断开了;小于0表示出现了错误并设置错误标志到errno全局变量中,如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题;

write()将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。

  1. write的返回值大于0,表示写了部分或者全部的数据。
  2. 返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。
    如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值