Socket学习

Socket概述

Socket定义

Socket可以说是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口。

在设计模式中,Socket其实就是门面模式,将复杂的TCP/IP协议族隐藏在Socket接口后边。

那么对用户来说,一组简单的接口就是需要学习的全部。交由Socket去组织数据,以符合指定的协议。


Socket流程

服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。

此时如果有客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这是客户端与服务器端的连接就建立了。

则客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。


其中,可以利用三元组(ip地址,协议,端口)来标识网络中的进程


那什么是socket呢?

起源于Unix,由于其基本哲学一切皆文件,都可以使用“打开open——>读写write/read——>关闭close”模式来操作。

Socket同样也是一种特殊的文件,很多socket函数就是对其进行的操作(读/写IO、打开、关闭)。


Socket函数解析

socket描述符创建参数

1、domian:协议域/协议族(family)

常用的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。

协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4(32位)和端口号(16位的)组合,而AF_UNIX决定了要用一个绝对路径名作为地址

2、type:指定socket类型

场景的socket类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等

3、protocol:指定协议

常用的协议有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应于TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议

(type和protocol并不能随意组合,如SOCK_STREAM不能与IPPROTO_UDP结合)

(当protocol为0时,会自动选择type类型对应的默认协议)

当调用了socket创建一个socket时,返回的socket描述字,存在于协议族(address family、AF_XXX空间中),但没有一个具体的地址。

因此如果想要给它赋值一个地址,就必须调用bind()函数,否则只有当调用connect()、listen()时系统才会自动随机分配一个端口。


通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器。

而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。

这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。


网络字节序与主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于 这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再 赋给socket。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。


read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网络中不同进程之间的通信!网络I/O操作有下面几组:

read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。

在网络程序中,当我们向套接字文件描述符写时有俩种可能。

1)write的返回值大于0,表示写了部分或者是 全部的数据。

2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。


close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。


C语言-Socket


头文件相关

#ifdef __unix
#include <sys/un.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#endif

#ifdef WIN32
#include <winsock2.h>
#include <afunix.h>
#endif

套接字文件描述符相关/创建socket相关

int clientFd = (int) socket(AF_INET, SOCK_STREAM, 0);
// clientFd代表客户端套接字文件描述符(File Descriptor),用于标识客户端与服务器之间的连接

设置socket选项

struct timeval timeout = {.tv_sec=SOCKET_TIMEOUT, .tv_usec=0};
if (setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof timeout) != 0 ||
    setsockopt(clientFd, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof timeout) != 0) {
    // SOL_SOCKET表示套接字级别的选项
    // SO_RCVTIMEO表示接受数据的超时时间,SO_SNDTIMEO表示发送数据的超时时间
    ERROR_LOG("setSockOption failed.");
    return ENOSYS;
    // 如果设置时报,则返回错误码 
}

// 其中
struct timeval
{
	long tv_sec; // 秒
	long tv_usec; // 微秒
};

创建服务器相关

例:创建IPv的服务器

struct sockaddr_in serverAddr; // 创建一个服务器的地址结构体
serverAddr.sin_family = AF_INET; // 将地址族设置为IPv4 ,是网络上通信的方式
serverAddr.sin_port = htons(port); // 将端口号设置为网络字节序(大端字节序)的形式
serverAddr.sin_addr.s_addr = inet_addr(ipAddr.buf); // 将IP地址设置为指定的字符串形式的IP地址,并将其转换为网络字节序的32位整数形式

例:创建本地机器不同进程间通信的服务器

struct sockaddr_un serverAddr;
serverAddr.sun_family = AF_UNIX; // 本地UNIX域套接字,是同一台机器上的进程间的通信方式
errno_t ret = strcpy_s(serverAddr.sun_path, sizeof(serverAddr.sun_path), sockPath.buf);

发送数据包和接收数据包相关

ssize_t transmitSize = send(socketSessionFd, request.buf, MIN(strlen(request.buf), request.len), 0);
if (transmitSize == -1) {
    ERROR_LOG("socket send request to server failed, errno(%d)", errno);
    return EPIPE;
}
DEBUG_LOG("send request size(%zd)", transmitSize); // 打印发送数据的长度

transmitSize = recv(socketSessionFd, response->buf, response->len > INT32_MAX ? INT32_MAX : (int) response->len, 0); // 打印接收数据的长度
if (transmitSize == -1) {
    ERROR_LOG("socket receive response from server failed, errno(%d)", errno);
    return EPIPE;
}
DEBUG_LOG("receive response size(%zd)", transmitSize);
response->buf[transmitSize] = '\0';
return EOK;

关闭socket

(void) shutdown(clientFd, SHUT_RDWR);
(void) close(clientFd);
// 关闭一个TCP连接的客户端套接字(socket)。
// 其中,shutdown()函数用于关闭套接字的读写功能,SHUT_RDWR参数表示同时关闭读和写。
// 而close()函数则用于释放套接字相关的资源,包括文件描述符和缓冲区等。这样,客户端和服务器之间的连接就被彻底关闭了。

END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-R00kie-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值