GVKun编程网logo

linux 本地socket(linux 本地源)

4

在这里,我们将给大家分享关于linux本地socket的知识,让您更了解linux本地源的本质,同时也会涉及到如何更有效地6、【Linux网络编程】socketsetsockopt()函数、C++中L

在这里,我们将给大家分享关于linux 本地socket的知识,让您更了解linux 本地源的本质,同时也会涉及到如何更有效地6、【Linux 网络编程】socket setsockopt () 函数、C++ 中 Linux 下 Socket 编程、epoll+socket实现 socket并发 linux服务器、golang socket与Linux socket比较分析的内容。

本文目录一览:

linux 本地socket(linux 本地源)

linux 本地socket(linux 本地源)

server 端:

void *recvData(void *arg)
{
    char recvbuf[1024];
    int conn = *(int *)arg;
    while(1)
    {
        memset(recvbuf,0,1024);
        recv(conn, recvbuf, sizeof(recvbuf),0);
    }
}

main 函数:
int listenfd;
int conn;
char sendbuf[1024];
char recvbuf[1024];
char sig_strength[16];
char radio_if[16];
if ((listenfd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
    LOGE("socket error");
unlink("/tmp/test_socket"); //地址复用
struct sockaddr_un servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path, "/tmp/test_socket");
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    LOGE("bind error");
if (listen(listenfd, SOMAXCONN) < 0)
    LOGE("listen error");
while(1)
{
    conn = accept(listenfd, NULL, NULL);
    if (conn == -1)
    {
        if (conn == EINTR)
            continue;
    }
    pthread_create(&thread_socket_recv, NULL, recvData, &conn);
    
    while(1)
    {
        memset(sendbuf,0,1024);
        strncpy(sendbuf,"xxxx, strlen("xxx"));
        send(conn, sendbuf, strlen(sendbuf),0);
    }
    close(conn);
    close(listenfd);
}

client端(非阻塞socket)
typedef struct QMISocket{
int socketfd;
struct timeval timeout;
pthread_mutex_t mMutex;
}QMISocket;
extern QMISocket mSocket;

static int sockConnect()
{
    struct sockaddr_un servaddr;
    int retval ;
    fd_set set;
    int flags;
    int ret=-1;
    
    if ((mSocket.socketfd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
        printf("socket error");
    mSocket.timeout.tv_sec = 3;
    mSocket.timeout.tv_usec = 0;
    setsockopt(mSocket.socketfd,SOL_SOCKET,SO_SNDTIMEO,(char *)&(mSocket.timeout),sizeof(struct timeval));
    setsockopt(mSocket.socketfd,SOL_SOCKET,SO_RCVTIMEO,(char *)&(mSocket.timeout),sizeof(struct timeval));
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sun_family = AF_UNIX;
    strcpy(servaddr.sun_path, "/tmp/test_socket");
    flags = fcntl(mSocket.socketfd, F_GETFL, 0);
    fcntl(mSocket.socketfd, F_SETFL, flags | O_NONBLOCK);
    ret = connect(mSocket.socketfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    if (0 == ret)
    {
        printf("connected\n");
    }
    else{
        if (errno != EINPROGRESS)
        {
		perror("connect");
		return-1;
	}
	FD_ZERO(&set);
	FD_SET(mSocket.socketfd,&set);
	retval = select(mSocket.socketfd+ 1,NULL, &set,NULL,&mSocket.timeout);
	if(retval==-1)
	{
	    perror("select");
	    return -1;
	}
	else if(retval== 0)
	{
		fprintf(stderr,"timeout\nn");
		printf("%d\n",time(NULL));
		return 0;
	}
	printf("connected\n");
	}
    }
    flags = fcntl(mSocket.socketfd, F_GETFL, 0);
    fcntl(mSocket.socketfd, F_SETFL, flags & ~O_NONBLOCK);
    return mSocket.socketfd;
}

int main()
{
    while(1)//wait the reboot-daemon
    {
        if (sockConnect() > 0)
            break;
        sleep(1);
    }
    while(1)
    {
        char recvbuf[1024]={0};  
        ret = recv(mSocket.socketfd, recvbuf, sizeof(recvbuf),0);
    }
}

这里在recv的时候,如果send端发送非常快,那么可能会导致一次recv到多个数据包的情况,那可需要在每个数据包结尾打上标记,表示一个个数据包。recv根据这些flags去截取每一个数据包

char  *remainBuf = NULL;
remainBuf = recvbuf;
char *tmp = strstr(remainBuf, "@");
memset(key,0,1024);
if (tmp != NULL)
{
	strncpy(key, remainBuf, tmp - remainBuf);
	remainBuf = remainBuf+(tmp - remainBuf)+1;
}


6、【Linux 网络编程】socket setsockopt () 函数

6、【Linux 网络编程】socket setsockopt () 函数

功能描述:

  获取或者设置与某个套接字关联的选项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该将层的值指定为 SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。例如,为了表示一个选项由 TCP 协议解析,层应该设定为协议号 TCP。

头文件:

1 #include <sys/types.h>
2 #include <sys/socket.h>

函数原型:

1 int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
2 int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);

参数:

1 sock:将要被设置或者获取选项的套接字。
2 level:选项所在的协议层。
3 optname:需要访问的选项名。
4 optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。
5 optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。

返回说明:

成功执行时,返回0。失败返回-1errno被设为以下的某个值  
EBADFsock不是有效的文件描述词
EFAULToptval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCKsock描述的不是套接字

 参数详细说明:

level 指定控制套接字的层次。可以取三种值:

1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项.

optname 指定控制的方式 (选项的名称)

选项名称        说明                  数据类型
========================================================================
            SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST      允许发送广播数据            int
SO_DEBUG        允许调试                int
SO_DONTROUTE      不查找路由                 int
SO_ERROR        获得套接字错误             int
SO_KEEPALIVE      保持连接                 int
SO_LINGER        延迟关闭连接              struct linger
SO_OOBINLINE      带外数据放入正常数据流          int
SO_RCVBUF        接收缓冲区大小              int
SO_SNDBUF        发送缓冲区大小              int
SO_RCVLOWAT       接收缓冲区下限               int
SO_SNDLOWAT       发送缓冲区下限             int
SO_RCVTIMEO       接收超时                struct timeval
SO_SNDTIMEO       发送超时                struct timeval
SO_REUSERADDR      允许重用本地地址和端口            int
SO_TYPE         获得套接字类型                int
SO_BSDCOMPAT      与 BSD 系统兼容                int
========================================================================
            IPPROTO_IP
------------------------------------------------------------------------
IP_HDRINCL       在数据包中包含 IP 首部            int
IP_OPTINOS       IP 首部选项                 int
IP_TOS         服务类型
IP_TTL         生存时间                    int
========================================================================
            IPPRO_TCP
------------------------------------------------------------------------
TCP_MAXSEG       TCP 最大数据段的大小           int
TCP_NODELAY       不使用 Nagle 算法               int
========================================================================

  SO_RCVBUF 和 SO_SNDBUF 每个套接口都有一个发送缓冲区和一个接收缓冲区,使用这两个套接口选项可以改变缺省缓冲区大小。

1  // 接收缓冲区
2 int nRecvBuf=32*1024;         //设置为32K
3 setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
4 
5 //发送缓冲区
6 int nSendBuf=32*1024;//设置为32K
7 setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));

结合原理说明:

  1. 每个套接口都有一个发送缓冲区和一个接收缓冲区。 接收缓冲区被 TCP 和 UDP 用来将接收到的数据一直保存到由应用进程来读。       TCP:TCP 通告另一端的窗口大小。 TCP 套接口接收缓冲区不可能溢出,因为对方不允许发出超过所通告窗口大小的数据。 这就是 TCP 的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方 TCP 将丢弃它。

   UDP:当接收到的数据报装不进套接口接收缓冲区时,此数据报就被丢弃。UDP 是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的 UDP 丢弃数据报。
  2. 我们经常听说 tcp 协议的三次握手,但三次握手到底是什么,其细节是什么,为什么要这么做呢?
        第一次:客户端发送连接请求给服务器,服务器接收;
        第二次:服务器返回给客户端一个确认码,附带一个从服务器到客户端的连接请求,客户机接收,确认客户端到服务器的连接.
        第三次:客户机返回服务器上次发送请求的确认码,服务器接收,确认服务器到客户端的连接.
        我们可以看到:
    (1)tcp 的每个连接都需要确认.
    (2)客户端到服务器和服务器到客户端的连接是独立的.
我们再想想 tcp 协议的特点:连接的,可靠的,全双工的,实际上 tcp 的三次握手正是为了保证这些特性的实现.

  3.setsockopt 的用法

  (1)closesocket(一般不会立即关闭而经历 TIME_WAIT 的过程)后想继续重用该 socket:

1 bool bReuseaddr=TRUE;
2 setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));
  (2)如果要已经处于连接状态的 soket 在调用 closesocket 后强制关闭,不经历 TIME_WAIT 的过程:
1 bool bDontLinger = FALSE;
2 setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));

  (3)在 send (),recv () 过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:

1 int nNetTimeout=1000;//1秒
2 //发送时限
3 setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
4 //接收时限
5 setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));

  (4)在 send () 的时候,返回的是实际发送出去的字节 (同步) 或发送到 socket 缓冲区的字节 (异步); 系统默认的状态发送和接收一次为 8688 字节 (约为 8.5K);在实际的过程中发送数据和接收数据量比较大,可以设置 socket 缓冲区,而避免了 send (),recv () 不断的循环收发:

1 // 接收缓冲区
2 int nRecvBuf=32*1024;//设置为32K
3 setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
4 //发送缓冲区
5 int nSendBuf=32*1024;//设置为32K
6 setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));

  (5)如果在发送数据的时,希望不经历由系统缓冲区到 socket 缓冲区的拷贝而影响程序的性能:

1 int nZero=0;
2 setsockopt(socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));

  (6)同上在 recv () 完成上述功能 (默认情况是将 socket 缓冲区的内容拷贝到系统缓冲区):

1 int nZero=0;
2 setsockopt(socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));

  (7)一般在发送 UDP 数据报的时候,希望该 socket 发送的数据具有广播特性:

1 bool bBroadcast=TRUE;
2 setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(bool));

  (8)在 client 连接服务器过程中,如果处于非阻塞模式下的 socket 在 connect () 的过程中可以设置 connect () 延时,直到 accpet () 被呼叫 (本函数设置只有在非阻塞的过程中有显著的作用,在阻塞的函数调用中作用不大)

1 bool bConditionalAccept=TRUE;
2 setsockopt(s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(bool));

  (9)如果在发送数据的过程中 (send () 没有完成,还有数据没发送) 而调用了 closesocket (), 以前我们一般采取的措施是 "从容关闭"shutdown (s,SD_BOTH), 但是数据是肯定丢失了,如何设置让程序满足具体应用的要求 (即让没发完的数据发送出去后在关闭 socket)?

1 struct linger {
2     u_short l_onoff;
3     u_short l_linger;
4 };
5 linger m_sLinger;
6 m_sLinger.l_onoff=1;//(在closesocket()调用,但是还有数据没发送完毕的时候容许逗留)
7 // 如果m_sLinger.l_onoff=0;则功能和2.)作用相同;
8 m_sLinger.l_linger=5;//(容许逗留的时间为5秒)
9 setsockopt(s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));

 

C++ 中 Linux 下 Socket 编程

C++ 中 Linux 下 Socket 编程

Socket 套接字是网络间不同计算机上的进程通信的一种常用方法,利用三元组(ip地址,协议,端口)就可以唯一标识网络中的进程,网络中的进程通信可以利用这个标志与其它进程进行交互。Socket 也是对 TCP/IP 协议族的一种封装,是应用层与 TCP/IP 协议族通信的中间软件抽象层。

1. Socket 基本概念与基本流程

Socket 起源于 Unix ,Unix/Linux 基本哲学之一就是一切皆文件,普通文件、目录、硬件设备、进程、管道是文件,Socket 也可以被认为是文件,所以也可以对 Socket 使用文件 I/O 的相关操作,可以用打开(open) –> 读写(read/write) –> 关闭(close)模式来进行操作。

Unix的另一创新是把磁盘、终端等外围设备都看作文件系统中的文件,磁盘是功能列表中提到的“可拆卸卷”。访问设备的系统调用和访问文件的系统调用是一样的,所以同样的代码既可以操作文件也可以操作设备。

今天的读者可能很难体会到这一切是做了多大简化之后的结果。早期操作系统中,真实设备的所有复杂情况都会反馈给用户。用户必须知道磁盘名称,了解磁盘的物理结构,如有多少柱面和磁道,以及数据是如何安放在上面。

——《UNIX传奇:历史与回忆》

Linux 平台下,socket() 返回的值被称为文件描述符 fd(File Descriptor),用来唯一标识一个套接字,在 Windows 平台它称为句柄 handle。本文用前者的叫法,下文句柄关键字一般用 fd 来表示。

套接字的主流程很简单,在服务端下,用 socket 创建套接字,使用 bind 分配 IP 地址和端口号,listen 将套接字转换成可受连接状态,开始监听前面分配的 IP 和端口号,然后调用 accept 受理连接请求,使用 write/read 来和客户端交换数据,使用 close 关闭连接。

客户端下就不需要 bind 分配 IP 和端口号,而是由 connect 自动分配端口号加上自身的 IP,然后使用 write/read 和服务端交换数据,最后 close 关闭连接。

Linux平台下的socket通信

Windows 平台和 Linux 的差别较小,在创建套接字之前需要调用 WSAStartUp(),关闭连接使用 closesocket(),关闭连接之后使用 WSACleanUp() 注销,其他没差别了,所以在项目中可以很方便地使用通过 _WIN32_ 宏来判断环境写跨端代码。

Win平台下的socket通信

2. 套接字创建

使用 socket() 函数创建一个套接字:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 创建套接字,返回 文件描述符,失败时返回-1
// domain: 套接字中使用的协议族(Protocol Family)
// type: 套接字数据传输的类型信息
// protocol: 计算机间通信中使用的协议信息

常用的套接字创建有两种:

int tcpFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);  
int udpFD = socket(PF_INET, SOCK_DGRAM,IPPROTO_UDP);

分别解释一下这两行,socket 第一个参数 PF_INET 表示是 IPv4 协议族,有时候用 AF_INET 宏,PF 的意思是 Protocol Family 协议族,AF 意为 Address Family,在 socket.h 文件中可以看到,其实是同一个值。

socket.h中的define

第一个参数还有其他取值 PF_INET6 表示 IPv6,PF_LOCAL 表示本地协议 UNIX 协议族,这些都不常用,还有其他的就更不常用了。

第二个参数是套接字类型,第三个是协议类型。下面重点介绍一下套接字类型,主要分两种 SOCK_STREAMSOCK_DGRAM

2.1 SOCK_STREAM 面向连接的套接字 TCP

TCP 用重传机制(Retransmission)保证数据一定传递到,如果指定时间内未收到某数据包的应答,则会重发来保证数据的完整性。

此外,即使接收方的缓存被填满,数据的传输也不会中断。TCP 会根据接收端的状态来调整数据传输速度,如果接收方的缓存已满,那么发送方将暂时停止发送数据,以防数据丢失。

传输数据没有数据边界,write 好几次,可能一次 read 就全取出,也可能 write 一次,需要 read 好几次才取出(粘包问题)。这是因为调用 write 函数时,write 在数据被移到输出 I/O 缓冲的时候就已经返回了,TCP 协议会保证在适当的时候(不管是分别传送还是一次性传送)传向对方的输入 I/O 缓冲,这时对方调用 read 函数会从输入缓冲读取数据。

TCP套接字的IO缓冲

每个套接字创建时,I/O 缓冲就会自动创建,并且每个套接字中单独存在。即使关闭套接字,输出缓冲中遗留的数据也会继续传递,但输入缓冲中的数据会丢失。

write 到输出缓冲区后,TCP 会将内容分段(Segmentation)并封装为 TCP 数据包进行发送。TCP 的封包主要是增加 TCP 首部,一些重要的字段比如 Win(窗口)、Seq(序列号)、Len(数据段长度)、Ack(确认号,确认接受到了哪些字节)等等,可以打开 wireshark 看一下具体的包。接收方在接收到这些数据包后,重新组装成原始的字节流。

TCP包

至于一次传输的数据量大小,则取决于 TCP 的滑动窗口(Sliding Window)和拥塞控制(Congestion Control),它们可以动态地调整 TCP 数据传输的速度以及窗口大小,以此来实现流量控制并防止网络拥塞。

这样的话,多次传输的数据要如何分辨内容的含义,这个字节是这个包还是那个包的呢?

TCP 确实无法分辨内容的边界,只能保证内容完整并正确地传输到了目的地,而分辨内容的边界只能在应用层协议中做,比如 HTTP 协议就会在头部增加 content-typecontent-length 等字段来帮助对方确定消息的边界,还有校验位等手段来保证消息的可靠性。

总结 TCP 套接字的特点如下:

  1. 传输过程中数据不会丢失(协议保证)。
  2. 按序传输数据。
  3. 传输的数据不存在数据边界。

2.2 SOCK_DGRAM 面向消息的套接字 UDP

如果说 TCP 是打电话,那么 UDP 就像发短信,短信更方便更快捷,但发出去并不知道对方有没有收到,传输的内容可能丢失也可能损毁,在强调速度的视频实时传输等场景,丢失一部分数据影响并不大,此时一般会使用 UDP。

为了提供可靠的数据传输服务,TCP 在不可靠的网络层(IP 层)进行流控制,而 UDP 就缺少这种流控制机制,而去除握手、挥手、拥塞和窗口控制等等流控制机制之后,TCP 和 UDP 的速度差别并不大,可以说流控制机制是 TCP 的灵魂。

UDP 套接字的特点如下:

  1. 强调快速传输而非传输顺序,后发也可能先至。
  2. 传输的数据有数据边界,write 几次,如果没有丢失的话也就需要 read 几次。
  3. 限制每次传输的数据大小。
  4. 传输的数据可能丢失也可能损毁。

3. 基于 TCP 的套接字

3.1 分配地址信息 bind

结构体 sockaddr 的声明在头文件 <sys/socket.h> 中:

struct sockaddr {       // in socket.h
    __uint8_t       sa_len;         /* total length */
    sa_family_t     sa_family;      /* [XSI] address family */
    char            sa_data[14];    /* [XSI] addr value (actually smaller or larger) */
};

地址和端口信息要存在 sa_data[] 这个字节数组中,但直接转换成字节赋值进去很麻烦,所以一般把数据放在 sockaddr_in 结构体中,然后在 bind 的时候强转一下类型。下面是一个典型的从创建套接字到 bind 的使用场景:

int sock_fd;
if ((sock_fd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
  printf("socket() error!");
}

struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // ip
serv_addr.sin_port = htons(atoi("9001"));            // port

if (bind(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {  // 注意这里的强制转换
  printf("bind() error!");
}

这里的 s_addr 有时候我们会这样赋值:

serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

此处 INADDR_ANY 的值为 0,表示任意 IP 地址,所以如果 s_addr 设置为 INADDR_ANY,意思是这个服务可以接受来自任意网络接口的连接请求。

3.1.1 端口号

端口号是在同一操作系统内为区分不同套接字而设置的,因此无法将一个端口号分配给不同套接字。

端口号 16 位,2 个字节,可分配的端口号范围是 0-655350-1023 一般分配给特定应用程序,所以应当分配此范围之外的值,由于是 2 个字节,所以 serv_addr.sin_port 这里需要的就是一个 USHORT 数据,对应 htons 函数,关于这个函数下面会有解释。

IP 地址 32 位,4 个字节,比如 255.255.255.255,每一个 255 是一个字节,由于是 4 个字节,所以 serv_addr.sin_addr.s_addr 这里需要的就是一个 unsigned long 数据,对应 htonl 函数,当然这里 inet_addr 直接返回的 unsigned long 就不用再转换了。

3.1.2 字节序转换

上面典型使用场景中 htonsntohshtonlntohl 这几个字节序转换函数,以 htons 为例:

htonsh 代表主机(host)字节序,主机可能是大端序也可能是小端序,不同系统的情况不一样,无论你的主机序是大端序还是小端序,都要加上这个。htonsn 代表网络(network)字节序,网络序一般为大端序。s 代表 shortl 代表 long

所以 htons 的意思就是:把 short 数据从网络字节序转化为主机字节序。

另外两个函数 inet_addratoi 还有个 inet_aton 也很常用:

  1. inet_addr:将 const char* 的用点号分隔开的十进制 IPv4 地址转换为网络字节序的 UINT,失败返回 INADDR_NONE
  2. inet_aton :将 const char* 的用点号分隔开的十进制 IPv4 地址转换为网络字节序的 UINT 并直接写入 sockaddr_in 结构体中的 in_addr,比如 inet_aton("127.0.0.1", &serv_addr.sin_addr),成功返回 0。
  3. net_ntoa:与 inet_aton 刚好相反,把 UINT 转换为 const char* 的 IPv4 地址。
  4. atoi: 将 const char* 转换为 int

3.2 服务端受理连接并通信 listen/accept

作为服务器,创建 socketbind 分配地址后,就会使用 listen 来监听套接字,套接字进入可发出连接请求的状态,此时还没有受理连接请求。listen 函数的第二个参数为连接请求等待队列的长度,表示最多多少个连接请求进入队列。如果等待连接队列已满,新的连接请求可能会被拒绝或丢失,因此需要适当设置等待连接队列的大小。

#include <sys/socket.h>
int listen(int serv_sock, int backlog);
// serv_sock: 希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数称为服务端套接字
// backlog: 连接请求等待队列的长度          

listen 之后,客户端套接字通过 connect 就可以进入等待连接队列了,服务端再 accept 就可以从等待连接队列中取出一个连接请求进行处理。

#include <sys/socket.h>
int accept(int serv_sock, struct sockaddr *clnt_addr, socklen_t *addrlen);
// serv_sock: 服务端套接字的文件描述符
// clnt_addr: 保存发起连接请求的客户端地址信息的变量地址值
// addrlen: 第二个参数的长度

如果在没有连接请求的情况下调用 acceptaccept 会阻塞等待客户端连接,当有新的客户端连接发起时,受理连接请求队列中待处理的客户端连接请求,函数调用成功后,accept 内部将产生用于数据 I/O 的套接字,并返回其文件描述符,新创建的套接字会继承监听套接字的地址和端口号,并自动与发起连接请求的客户端建立连接。

强调一下,原来的 socket 创建的是监听套接字,是监听连接请求队列的,而 accept 成功返回后的文字描述符是已经和发起连接请求的客户端连接后的连接套接字accept 成功返回新的连接套接字后,就可以通过给连接套接字用 read/write 向连接的客户端发送和接受数据了,原来的监听套接字仍然处于监听状态。所以服务器同时处理多个连接请求的时候,是每个连接都有自己的套接字独立地进行通信的,服务器可以用这些套接字与不同的客户端进行交互。

所以一个服务端通常只维护一个监听套接字,在这个服务端的生命周期内始终存在,通过该套接字接受多个客户端的连接请求,并为每个连接创建独立的连接套接字,以实现与不同客户端的通信,当服务端与某个客户端完成服务后关闭相对应地连接套接字。这样,服务器可以同时处理多个连接,提高系统的并发性和可扩展性。

3.3 客户端发起连接并通信 connect

客户端通过 socket 创建套接字后用 connet 就可以向服务端发起连接请求了,但需要服务器端 listen 进入监听状态后才能 connect,此时客户端的连接请求进入到服务端的等待连接队列等待被 accept 取用。

#include <sys/socket.h>
int connect(int clnt_sock, struct sockaddr *serv_addr, socklen_t addrlen);
// clnt_sock:客户端套接字文件描述符
// serv_addr: 目标服务器端地址信息的变量地址值
// addrlen: 第二个参数的长度

客户端套接字的 IP 地址和端口其实也是需要的,但是在调用 connect 时操作系统会自动分配,无需调用 bind 进行分配。

随后 TCP 建立连接的过程就是三次握手(Three-way Handshaking),断开链接的过程就是四次挥手(Four-way Handshaking)了。

3.4 发送与接收数据 write/read

发送与接收数据提供了多组 I/O 函数:

  1. read / write:向任意文件描述符中读/写数据,只能向已经建立连接的文件描述符中读/写数据。
  2. recv / send:向任意文件描述符中读/写多个缓冲区的数据,只能向已经建立连接的文件描述符中读/写数据。
  3. readv / writevwritev 将多个散布在不同缓冲区中的数据从iovec数组写入到套接字,readv 读取多个散布在不同缓冲区中的数据,将其存储到iovec数组中的多个缓冲区中。
  4. recvmsg / sendmsgrecvmsg 从套接字中接收数据并存储在 msghdr 结构体中,允许接收更多的附加信息,比如控制消息,sendmsg 则从 msghdr 结构体发信息到套接字中。

这些 I/O 函数具有不同功能,用在不同场景,活学活用。

3.5 结束连接 close

类似于关闭文件,在读写操作完成后关闭相应的描述字也是使用 close

#include <unistd.h>
int close(int fd);
// 成功时返回 0 ,失败时返回 -1
// fd : 需要关闭的套接字的文件描述符

结束链接调用的 close 其实就是向服务端发送了 EOF

3.5.1 优雅断开

某些情况下,通信一方单方面的断开套接字连接,显得不太优雅。

比如客户端接收到服务端发的最后消息后还回传一个消息,但此时服务端如果已经 close 就会丢失回传的消息。此时就需要只关闭一部分数据交换中使用的流,断开一部分连接指可以传输数据但是无法接收,或可以接受数据但无法传输,称为半关闭 Half-close。

#include <sys/socket.h>
int shutdown(int sock, int howto);
// 成功时返回 0 ,失败时返回 -1
// sock: 需要断开套接字文件描述符
// howto: 传递断开方式信息
//     SHUT_RD : 断开输入流,套接字无法接受数据
//     SHUT_WR : 断开输出流,套接字无法传输数据
//     SHUT_RDWR : 同时断开 I/O 流

3.5.2 Time-Wait 状态

当使用 TCP 协议进行通信时,其中一方主动关闭连接后,会进入 Time-Wait 状态。在该状态下,该端口在一段时间内不接受新的连接请求。这个时间段通常是两倍的最大段生存时间 MSL(Maximum Segment Lifetime),MSL 的值在不同操作系统中可能有所不同,一般为 30 秒到几分钟。

Time-Wait 状态的持续时间是为了保证网络中所有的剩余数据包都能够传递到目的地或被丢弃,以及确保所有的连接关闭消息都能够传达给对方。在 Time-Wait 状态下,端口会保持开放,以便接收对方可能发送的最后一个确认(ACK)包。这样做可以确保双方都能成功关闭连接,并防止已关闭连接的端口被新的连接误用。此时如果连接,会报 bind() error 错,需要等待一段时间之后才可以重新连这个端口。

Time-Wait 不仅存在于服务器端,不管是服务器端还是客户端,先断开连接的套接字必然会有 Time-Wait。但客户端 Time-Wait 并没有多少感知,因为客户端每次都会动态分配端口号,因此无需过多关注 Time-Wait 状态。

通常,Time-Wait 状态对网络应用的正常运行没有太大影响,但如果服务器需要频繁地关闭和重新打开连接,Time-Wait 可能会导致端口资源的消耗。此时可以通过调整操作系统的参数来缩短 Time-Wait 状态的持续时间,或设置重用地址 SO_REUSEADDR 参数来更好地管理端口资源。

setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);

3.6 TCP 服务端实现

在客户端读到什么就原样发回去的实现:

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

#define BUF_SIZE 1024
void error_handling(char *message) {
  fputs(message, stderr);
  fputc(''\n'', stderr);
  exit(1);
}

int main(int argc, char *argv[]) {
  int serv_sock, clnt_sock;
  char message[BUF_SIZE];
  int str_len, i;

  struct sockaddr_in serv_adr, clnt_adr;
  socklen_t clnt_adr_sz;

  serv_sock = socket(PF_INET, SOCK_STREAM, 0);      // 创建套接字
  if (serv_sock == -1)  error_handling("socket() error");

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;                   // ipv4
  serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听任意地址的请求
  serv_adr.sin_port = htons(atoi("60001"));        // 端口号

  if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) // 分配地址
    error_handling("bind() error");

  if (listen(serv_sock, 5) == -1)     // 开始监听
    error_handling("listen() error");

  clnt_adr_sz = sizeof(clnt_adr);
  
  // 调用 5 次 accept 函数,共为 5 个客户端提供服务
  for (i = 0; i < 5; i++) {
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);   // 受理连接
    if (clnt_sock == -1) error_handling("accept() error");
    else printf("Connect client %d : %s \n", i + 1, inet_ntoa(clnt_adr.sin_addr));

    while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0) {    // 读到消息就处理
      printf("Message : %s   from client %s \n", message, inet_ntoa(clnt_adr.sin_addr));
      write(clnt_sock, message, str_len);   // 把读的发回去
    }
    close(clnt_sock);    // 别忘了关
  }
  close(serv_sock);      // 别忘了关
  return 0;
}

3.7 TCP 客户端实现

一个输入什么就发送什么的简单 TCP 客户端最小实现:

// 头部参考 3.6 节的代码
int main(int argc, char *argv[]) {
  int sock, str_len;
  char message[BUF_SIZE];
  struct sockaddr_in serv_adr;

  sock = socket(PF_INET, SOCK_STREAM, 0);      // 创建
  if (sock == -1)
    error_handling("socket() error");

  memset(&serv_adr, 0, sizeof(serv_adr)); 
  serv_adr.sin_family = AF_INET;                       // ipv4
  serv_adr.sin_addr.s_addr = inet_addr("127.0.0.1");   // 在本地起一个客户端
  serv_adr.sin_port = htons(atoi("60001"));            // 端口号

  if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)    // 客户端发起链接
    error_handling("connect() error!");
  else puts("Connected...........");

  while (1) {
    fputs("Input message(Q to quit): ", stdout);  // 打印啥发送啥
    fgets(message, BUF_SIZE, stdin);              // 从终端读

    if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) // 输入Q就退出
      break;

    write(sock, message, strlen(message));       // 写
    str_len = read(sock, message, BUF_SIZE - 1); // 读
    message[str_len] = 0;
    printf("Message from server: %s", message);
  }
  close(sock);    // 别忘了关
  return 0;
}

4. 基于 UDP 的套接字

由于 UDP 是无连接的传输协议,所以基于 UDP 的套接字无需经过连接过程,包括 listenacceptconnect,只需要创建套接字,然后就可以发送和接收消息了。

UDP 不同于 TCP,不存在谁 listen 或者谁是 connect 一方,因此在某种意义上无法明确区分服务器端和客户端。

4.1 发送与接收数据 sendto/recvfrom

创建好 TCP 套接字以后,传输数据时无需加上地址信息。因为 TCP 套接字将保持与对方套接字的连接。换言之,TCP 套接字知道目标地址信息。但 UDP 套接字不会保持连接状态(UDP 套接字只有简单的邮筒功能),因此每次传输数据时都需要添加目标的地址信息。这相当于寄信前在信件中填写地址。

#include <sys/socket.h>
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags,
               struct sockaddr *to, socklen_t addrlen);
// 向目标地址发送UDP信息,成功时返回传输的字节数,失败是返回 -1
// sock: 用于传输数据的 UDP 套接字
// buff: 保存待传输数据的缓冲地址值
// nbytes: 待传输的数据长度,以字节为单位
// flags: 可选项参数,若没有则传递 0
// to: 存有目标地址的 sockaddr 结构体变量的地址值
// addrlen: 传递给参数 to 的地址值结构体变量长度

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
                 struct sockaddr *from, socklen_t *addrlen);
// 读取某地址信息发送过来的UDP信息,成功时返回传输的字节数,失败是返回 -1
// sock: 用于接收数据的UDP套接字文件描述符
// buff: 保存接收数据的缓冲地址值
// nbytes: 可接收的最大字节数,故无法超过参数buff所指的缓冲大小
// flags: 可选项参数,若没有则传递 0
// from: 结果参数,存有发送端地址信息的 sockaddr 结构体变量的地址值
// addrlen: 保存参数 from 的结构体变量长度的变量地址值。

所有套接字都应分配 IP 地址和端口,只是直接分配还是自动分配的区别,TCP 和 UDP 的服务端需要 bind 一个本地的端口(地址一般用 INADDR_ANY),TCP 客户端套接字是在调用 connect 函数时自动分配 IP 和端口号,而 UDP 的客户端一般在首次调用 sendto 的时候会给套接字自动分配 IP (主机 IP)和端口,分配的 IP 和端口号会保留到程序结束。

cpp-socket通信linux_udp

与 TCP 发送多次一次就可以接收到不同,UDP 里 sendto 发送几次,就需要调用几次 recvfrom

4.2 已连接 UDP 套接字

TCP 套接字中需注册待传传输数据的目标 IP 和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段:

  • 第 1 阶段:向 UDP 套接字注册目标 IP 和端口号
  • 第 2 阶段:传输数据
  • 第 3 阶段:删除 UDP 套接字中注册的目标地址信息。

每次调用 sendto 函数时重复上述过程,每次使用都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。要与同一主机进行长时间通信时,将 UDP 套接字变成已连接套接字会节约第 1 和第 3 阶段的时间,从而提高整体性能。

与 TCP 类似,UDP 套接字默认没调用 connect 就属于未连接套接字,没有注册目标地址信息。反之,注册了目标地址的套接字称为已连接套接字 connected

使用 connect 函数后,并不会建立像 TCP 那样的可靠连接,UDP 仍然是无连接的,只是向 UDP 套接字注册目标 IP 和端口信息,绑定了默认的地址。如果不 connect,则需要在每次 sendto 时指定目标地址和端口,在 recvfrom 时获取数据的源地址信息。

connect 之后就与 TCP 套接字一致,每次调用 sendto 函数时只需传递信息数据。因为已经指定了默认的收发地址信息,所以不仅可以使用 sendtorecvfrom 函数,还可以使用 writeread,这两个函数相比前者,少了每次调用时传入指定目标地址参数,使用的是 connect 指定的默认地址。

4.3 UDP 服务端实现

// 头部参考 3.6 节的代码
int main(int argc, char *argv[]) {
  int serv_sock, str_len;
  char message[BUF_SIZE];
  socklen_t clnt_adr_sz;

  struct sockaddr_in serv_adr, clnt_adr;

  // socket 的第二个参数传递 SOCK_DGRAM
  serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
  if (serv_sock == -1)
    error_handling("UDP socket creation eerror");

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;                 // ipv4
  serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);  // 接受任意地址的请求
  serv_adr.sin_port = htons(atoi("60001"));      // 端口号
  // 分配地址接受数据,不限制数据传输对象
  if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)   // 绑定地址信息
    error_handling("bind() error");

  while (1) {
    clnt_adr_sz = sizeof(clnt_adr);
    str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);  // 读
    printf("Received data: %s from:%s\n", message, inet_ntoa(clnt_adr.sin_addr));
    
    // 通过上面的函数调用同时获取数据传输端的地址,利用该地址进行逆向重传
    sendto(serv_sock, message, str_len, 0, (struct sockaddr *)&clnt_adr, clnt_adr_sz);
  }
  close(serv_sock);    // 别忘了关
  return 0;
}

4.4 UDP 客户端实现

一个输入什么就发送什么的简单 UDP 客户端最小实现:

// 头部参考 3.6 节的代码
int main(int argc, char *argv[])
{
    int sock, str_len;
    char message[BUF_SIZE];
    socklen_t adr_sz;

    struct sockaddr_in serv_adr, from_adr;

    sock = socket(PF_INET, SOCK_DGRAM, 0);    // 创建 UDP 套接字
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;                   // ipv4
    serv_adr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 本地起客户端
    serv_adr.sin_port = htons(atoi("60001"));        // 端口号

    while (1) {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
        // 向服务器传输数据,自动给自己分配IP地址和端口号
        sendto(sock, message, strlen(message), 0,
               (struct sockaddr *)&serv_adr, sizeof(serv_adr));
        adr_sz = sizeof(from_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&from_adr, &adr_sz);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
    close(sock);    // 别忘了关
    return 0;
}

5. 域名和 IP 互转(DNS)

DNS(Domain Name System) 是对 IP 地址和域名进行相互转换的系统。比如向一个域名发起请求后,首先会去向上一级的 DNS 服务器去查询,通过这种方式逐级向上传递信息,一直到达根服务器,它知道应该向哪个 DNS 服务器发起询问。再向下传递解析请求,得到 IP 地址候原路返回,最后会将解析的IP地址传递到发起请求的主机。

使用下面两个函数可将域名转化为 IP,或者将 IP 转化为域名,返回的信息在 hostent 结构体中。

#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
// 域名转IP。成功时返回 hostent 结构体地址,失败时返回 NULL 指针

struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);
// IP转域名。成功时返回 hostent 结构体变量地址值,失败时返回 NULL 指针
// addr: 含有IP地址信息的 in_addr 结构体指针。为了同时传递 IPV4 地址之外的全部信息,该变量的类型声明为 char 指针
// len: 向第一个参数传递的地址信息的字节数,IPV4时为 4 ,IPV6 时为16.
// family: 传递地址族信息,ipv4 是 AF_INET ,IPV6是 AF_INET6

关于 socket 编程的更多内容,比如多线程服务器端、epoll 等,我们将在下一篇文章里继续讨论。


网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出,如果本文帮助到了你,别忘了点赞支持一下哦,你的点赞是我更新的最大动力!\~

参考文档:

  1. TCP/IP网络编程

PS:本文收录在在下的博客 Github - SHERlocked93/blog 系列文章中,欢迎大家关注我的公众号 CPP下午茶,直接搜索即可添加,持续为大家推送 CPP 以及 CPP 周边相关优质技术文,共同进步,一起加油\~

另外可以加入「前端下午茶交流qun」,vx 搜索 sherlocked_93 加我,备注 1,我拉你~

epoll+socket实现 socket并发 linux服务器

epoll+socket实现 socket并发 linux服务器

/* 实现功能:通过epoll, 处理多个socket
 * 监听一个端口,监听到有链接时,添加到epoll_event
 * xs
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <netinet/in.h>
 
#define MYPORT 12345
 
//最多处理的connect
#define MAX_EVENTS 500
 
//当前的连接数
int currentClient = 0; 
 
//数据接受 buf
#define REVLEN 10
char recvBuf[REVLEN];
 
 
//epoll描述符
int epollfd;
//事件数组
struct epoll_event eventList[MAX_EVENTS];
 
void AcceptConn(int srvfd);
void RecvData(int fd);
 
int main()
{
    int i, ret, sinSize;
    int recvLen = 0;
    fd_set readfds, writefds;
    int sockListen, sockSvr, sockMax;
    int timeout;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    
    //socket
    if((sockListen=socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("socket error\n");
        return -1;
    }
    
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family  =  AF_INET;
    server_addr.sin_port = htons(MYPORT);
    server_addr.sin_addr.s_addr  =  htonl(INADDR_ANY); 
    
    //bind
    if(bind(sockListen, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0)
    {
        printf("bind error\n");
        return -1;
    }
    
    //listen
    if(listen(sockListen, 5) < 0)
    {
        printf("listen error\n");
        return -1;
    }
    
    // epoll 初始化
    epollfd = epoll_create(MAX_EVENTS);
    struct epoll_event event;
    event.events = EPOLLIN|EPOLLET;
    event.data.fd = sockListen;
    
    //add Event
    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockListen, &event) < 0)
    {
        printf("epoll add fail : fd = %d\n", sockListen);
        return -1;
    }
    
    //epoll
    while(1)
    {
        timeout=3000;                
        //epoll_wait
        int ret = epoll_wait(epollfd, eventList, MAX_EVENTS, timeout);
        
        if(ret < 0)
        {
            printf("epoll error\n");
            break;
        }
        else if(ret == 0)
        {
            printf("timeout ...\n");
            continue;
        }
        
        //直接获取了事件数量,给出了活动的流,这里是和poll区别的关键
        int i = 0;
        for(i=0; i<ret; i++)
        {
            //错误退出
            if ((eventList[i].events & EPOLLERR) ||
                (eventList[i].events & EPOLLHUP) ||
                !(eventList[i].events & EPOLLIN))
            {
              printf ( "epoll error\n");
              close (eventList[i].data.fd);
              return -1;
            }
            
            if (eventList[i].data.fd == sockListen)
            {
                AcceptConn(sockListen);
        
            }else{
                RecvData(eventList[i].data.fd);
            }
        }
    }
    
    close(epollfd);
    close(sockListen);
    
 
    return 0;
}
 
/**************************************************
函数名:AcceptConn
功能:接受客户端的链接
参数:srvfd:监听SOCKET
***************************************************/
void AcceptConn(int srvfd)
{
    struct sockaddr_in sin;
    socklen_t len = sizeof(struct sockaddr_in);
    bzero(&sin, len);
 
    int confd = accept(srvfd, (struct sockaddr*)&sin, &len);
 
    if (confd < 0)
    {
       printf("bad accept\n");
       return;
    }else
    {
        printf("Accept Connection: %d", confd);
    }
    //将新建立的连接添加到EPOLL的监听中
    struct epoll_event event;
    event.data.fd = confd;
    event.events =  EPOLLIN|EPOLLET;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event);
}
 
//读取数据
void RecvData(int fd)
{
    int ret;
    int recvLen = 0;
    
    memset(recvBuf, 0, REVLEN);
    printf("RecvData function\n");
    
    if(recvLen != REVLEN)
    {
        while(1)
        {
            //recv数据
            ret = recv(fd, (char *)recvBuf+recvLen, REVLEN-recvLen, 0);
            if(ret == 0)
            {
                recvLen = 0;
                break;
            }
            else if(ret < 0)
            {
                recvLen = 0;
                break;
            }
            //数据接受正常
            recvLen = recvLen+ret;
            if(recvLen<REVLEN)
            {
                continue;
            }
            else
            {
                //数据接受完毕
                printf("buf = %s\n",  recvBuf);
                recvLen = 0;
                break;
            }
        }
    }
 
    printf("data is %s", recvBuf);
}

golang socket与Linux socket比较分析

golang socket与Linux socket比较分析

       在posix标准推出后,socket在各大主流OS平台上都得到了很好的支持。而Golang是自带runtime的跨平台编程语言,Go中提供给开发者的socket API是建立在操作系统原生socket接口之上的。但golang 中的socket接口在行为特点与操作系统原生接口有一些不同。本文将对结合一个简单的hello/hi的网络聊天程序加以分析。

    一、socket简介

       首先进程之间可以进行通信的前提是进程可以被唯一标识,在本地通信时可以使用PID唯一标识,而在网络中这种方法不可行,我们可以通过IP地址+协议+端口号来唯一标识一个进程,然后利用socket进行通信。

       socket是位于应用层和传输层中的抽象层,它是不属于七层架构中的:

                                                     

     而socket通信流程如下:

1.服务端创建socket

2.服务端绑定socket和端口号

3.服务端监听该端口号

4.服务端启动accept()用来接收来自客户端的连接请求,此时如果有连接则继续执行,否则将阻塞在这里。

5.客户端创建socket

6.客户端通过IP地址和端口号连接服务端,即tcp中的三次握手

7.如果连接成功,客户端可以向服务端发送数据

8.服务端读取客户端发来的数据

9.任何一端均可主动断开连接

                                                     

 二、socket编程

    有了抽象的socket后,当使用TCP或UDP协议进行web编程时,可以通过以下的方式进行

服务端伪代码:

listenfd = socket(……)
bind(listenfd, ServerIp:Port, ……)
listen(listenfd, ……)
while(true) {
  conn = accept(listenfd, ……)
  receive(conn, ……)
  send(conn, ……)
}

客户端伪代码:

clientfd = socket(……)
connect(clientfd, serverIp:Port, ……)
send(clientfd, data)
receive(clientfd, ……)
close(clientfd)

      上述伪代码中,listenfd就是为了实现服务端监听创建的socket描述符,而bind方法就是服务端进程占用端口,避免其它端口被其它进程使用,listen方法开始对端口进行监听。下面的while循环用来处理客户端源源不断的请求,accept方法返回一个conn,用来区分各个客户端的连接的,之后的接受和发送动作都是基于这个conn来实现的。其实accept就是和客户端的connect一起完成了TCP的三次握手。

三、golang中的socket

      golang中提供了一些网络编程的API,包括Dial,Listen,Accept,Read,Write,Close等.

3.1 Listen()

     首先使用服务端net.Listen()方法创建套接字,绑定端口和监听端口。

1 func Listen(network, address string) (Listener, error) {
2     var lc ListenConfig
3     return lc.Listen(context.Background(), network, address)
4 }

      以上是golang提供的Listen函数源码,其中network表示网络协议,如tcp,tcp4,tcp6,udp,udp4,udp6等。address为绑定的地址,返回的Listener实际上是一个套接字描述符,error中保存错误信息。

     而在Linuxsocket中使用socket,bind和listen函数来完成同样功能

// socket(协议域,套接字类型,协议)
int socket(int domain, int type, int protocol);

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

int listen(int sockfd, int backlog);

3.2 Dial()

   当客户端想要发起某个连接时,就会使用net.Dial()方法来发起连接

func Dial(network, address string) (Conn, error) {
    var d Dialer
    return d.Dial(network, address)
}

     其中network表示网络协议,address为要建立连接的地址,返回的Conn实际是标识每一个客户端的,在golang中定义了一个Conn的接口:

type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    LocalAddr() Addr
    RemoteAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

type conn struct {
    fd *netFD
}

     其中netFD是golang网络库里最核心的数据结构,贯穿了golang网络库所有的API,对底层的socket进行封装,屏蔽了不同操作系统的网络实现,这样通过返回的Conn,我们就可以使用golang提供的socket底层函数了。

  在Linuxsocket中使用connect函数来创建连接

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

3.3 Accept()

       当服务端调用net.Listen()后会开始监听指定地址,而客户端调用net.Dial()后发起连接请求,然后服务端调用net.Accept()接收请求,这里端与端的连接就建立好了,实际上到这一步也就完成了TCP中的三次握手。

Accept() (Conn, error)

      golang的socket实际上是非阻塞的,但golang本身对socket做了一定处理,使其看起来是阻塞的。

      在Linuxsocket中使用accept函数来实现同样功能

//sockfd是服务器套接字描述符,sockaddr返回客户端协议地址,socklen_t是协议地址长度。
int
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

3.4 Write()

      端与端的连接已经建立了,接下来开始进行读写操作,conn.Write()向socket写数据

   Write(b []byte) (n int, err error)
func (c *conn) Write(b []byte) (int, error) {
    if !c.ok() {
        return 0, syscall.EINVAL
    }
    n, err := c.fd.Write(b)
    if err != nil {
        err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return n, err
}

    其中写入的数据是一个二进制字节流,n返回的数据的长度,err保存错误信息

    Linuxsocket中对应的则是send函数

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

3.5 Read()

     客户端发送完数据以后,服务端可以接收数据,golang中调用conn.Read()读取数据,源码如下:

Read(b []byte) (n int, err error)
func (c *conn) Read(b []byte) (int, error) {
    if !c.ok() {
        return 0, syscall.EINVAL
    }
    n, err := c.fd.Read(b)
    if err != nil && err != io.EOF {
        err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    return n, err
}

      其参数与Write()中的含义一样,在Linuxsocket中使用recv函数完成此功能

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

3.6 Close()

     当服务端或者客户端想要关闭套接字时,调用Close()方法关闭连接。

Close() error
func (c
*conn) Close() error { if !c.ok() { return syscall.EINVAL } err := c.fd.Close() if err != nil { err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return err }

    在Linuxsocket中使用close函数

int close(int socketfd)

四、golang实现Hello/hi网络聊天程序

4.1 server.go

package main
import (
    "fmt"
    "net"
    "strings"
)
//UserMap保存的是当前聊天室所有用户id的集合
var UserMap map[string]net.Conn = make(map[string]net.Conn)
func main() {
    //监听本地所有ip的8000端口
    listen_socket, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("服务启动失败")
    }
    //关闭监听的端口
    defer listen_socket.Close()
    fmt.Println("等待用户加入聊天室")
    for {
        //用于conn接收链接
        conn, err := listen_socket.Accept()
        if err != nil {
            fmt.Println("连接失败")
        }
        //打印加入聊天室的公网IP地址
        fmt.Println(conn.RemoteAddr(), "连接成功")
        //定义一个goroutine,这里主要是为了并发运行
        go DataProcessing(conn)
    }
}
func DataProcessing(conn net.Conn) {
    for {
        //定义一个长度为255的切片
        data := make([]byte, 255)
        //读取客户端传来的数据,msg_length保存长度,err保存错误信息
        msg_length, err := conn.Read(data)
        if msg_length == 0 || err != nil {
            continue
        }
        //解析协议,通过分隔符"|"获取需要的数据,msg_str[0]存放操作类别
        //msg_str[1]存放用户名,msg_str[2]如果有就存放发送的消息
        msg_str := strings.Split(string(data[0:msg_length]), "|")
        switch msg_str[0] {
        case "nick":
            fmt.Println(conn.RemoteAddr(), "的用户名是", msg_str[1])
            for user, message := range UserMap {
                //向除自己之外的用户发送加入聊天室的消息
                if user != msg_str[1] {
                    message.Write([]byte("用户" + msg_str[1] + "加入聊天室"))
                }
            }
            //将该用户加入用户id的集合
            UserMap[msg_str[1]] = conn
        case "send":
            for user, message := range UserMap {
                if user != msg_str[1] {
                    fmt.Println("Send "+msg_str[2]+" to ", user)
                    //向除自己之外的用户发送聊天消息
                    message.Write([]byte("       用户" + msg_str[1] + ": " + msg_str[2]))
                }
            }
        case "quit":
            for user, message := range UserMap {
                if user != msg_str[1] {
                    //向除自己之外的用户发送退出聊天室的消失
                    message.Write([]byte("用户" + msg_str[1] + "退出聊天室"))
                }
            }
            fmt.Println("用户 " + msg_str[1] + "退出聊天室")
            //将该用户名从用户id的集合中删除
            delete(UserMap, msg_str[1])
        }
    }
}

5.2 client.go

package main
import (
    "bufio"
    "fmt"
    "net"
    "os"
)
var nick string = ""
func main() {
    //拨号操作
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("连接失败")
    }
    defer conn.Close()
    fmt.Println("连接服务成功 \n")
    //创建用户名
    fmt.Printf("在进入聊天室之前给自己取个名字吧:")
    fmt.Scanf("%s", &nick)
    fmt.Println("用户" + nick + "欢迎进入聊天室")
    //向服务器发送数据
    conn.Write([]byte("nick|" + nick))
    //定义一个goroutine,这里主要是为了并发运行
    go SendMessage(conn)
    var msg string
    for {
        msg = ""
        //由于golangz的fmt包输入字符串不能读取空格,所以此处重写了一个Scanf函数
        Scanf(&msg)
        if msg == "quit" {
            //这里的quit,send,以及上面的nick是为了识别客户端做的是设置用户名,发消息还是退出
            conn.Write([]byte("quit|" + nick))
            break
        }
        if msg != "" {
            conn.Write([]byte("send|" + nick + "|" + msg))
        }
    }
}
func SendMessage(conn net.Conn) {
    for {
        //定义一个长度为255的切片
        data := make([]byte, 255)
        //读取服务器传来的数据,msg_length保存长度,err保存错误信息
        msg_length, err := conn.Read(data)
        if msg_length == 0 || err != nil {
            break
        }
        fmt.Println(string(data[0:msg_length]))
    }
}
//重写的Scanf函数
func Scanf(a *string) {
    reader := bufio.NewReader(os.Stdin)
    data, _, _ := reader.ReadLine()
    *a = string(data)
}

      golang中使用goroutine实现并发

5.3 运行截图

多人聊天截图(左上角为服务端)

用户退出聊天室(左上角为服务端)

 

 参考资料:

     https://tonybai.com/2015/11/17/tcp-programming-in-golang/

     https://www.jianshu.com/p/325ac02fc31c

     https://blog.csdn.net/dyd961121/article/details/81252920

关于linux 本地socketlinux 本地源的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于6、【Linux 网络编程】socket setsockopt () 函数、C++ 中 Linux 下 Socket 编程、epoll+socket实现 socket并发 linux服务器、golang socket与Linux socket比较分析等相关知识的信息别忘了在本站进行查找喔。

本文标签: