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

原文地址:https://www.cnblogs.com/ustc-kunkun/p/11990285.html

时间: 2024-11-02 21:01:26

golang socket与Linux socket比较分析的相关文章

Windows Socket和Linux Socket编程的区别 ZZ

socket相关程序从Windows移植到Linux下需要注意的: 1)头文件 Windows下winsock.h/winsock2.h Linux下sys/socket.h 错误处理:errno.h 2)初始化 Windows下需要用WSAStartup Linux下不需要 3)关闭socket Windows下closesocket(...) Linux下close(...) 4)类型 Windows下SOCKET Linux下int 如我用到的一些宏: #ifdef WIN32 typed

【转】Windows Socket和Linux Socket编程有什么区别

socket相关程序从Windows移植到Linux下需要注意的: 1)头文件 Windows下winsock.h/winsock2.h Linux下sys/socket.h 错误处理:errno.h 2)初始化 Windows下需要用WSAStartup Linux下不需要 3)关闭socket Windows下closesocket(...) Linux下close(...) 4)类型 Windows下SOCKET Linux下int 如我用到的一些宏: #ifdef WIN32 typed

linux socket编程系统调用栈

目录 一.网络协议参考模型简介 二.SOCKET概述 三.SOCKET基本数据结构 1.TCP通信编程 2.服务器端实例代码 3.客户端实例代码 4.头文件socketwrapper.h 5.程序实现功能 6.探究socket系统调用 @(linux socket编程实现原理) 一.网络协议参考模型简介 国际标准组织(ISO)制定了OSI模型.这个模型把网络通信的工作分为7层,从上至下为应用层.表示层.会话层. 传输层.网络层.数据链路层.物理层. 而TCP/IP协议将OSI的7层模型简化为4层

关于socket在Linux下系统调用的分析

1.在include/linux/syscalls.h中定义了sys_socket函数的函数原型 asmlinkage long sys_socket(int, int, int); 系统调用函数必须满足: asmlinkage long sys_##function-name(##args){ ,return ret} 2.在arch/arm/include/asm,unistd.h中,将sys_socket系统调用和系统调用好关联起来 #define __NR_socket 97 __SYS

通过基于java实现的网络聊天程序分析java中网络API和Linux Socket API关系

1. 引言 socket网络编程,可以指定不同的通信协议,在这里,我们使用TCP协议实现基于java的C/S模式下“hello/hi”网络聊天程序 2. 目标 1). 通过该网络聊天程序,了解java Socket API接口的基本用法 2). java Socket API简要介绍 3). linux socket API 简单分析 4). tcp协议的连接和终止 5). 探究java socket API 和 linux socket api之间的关系 3. linux socket API

Linux socket编程 DNS查询IP地址

本来是一次计算机网络的实验,但是还没有完全写好,DNS的响应请求报文的冗余信息太多了,不只有IP地址.所以这次的实验主要就是解析DNS报文.同时也需要正确的填充请求报文.如果代码有什么bug,欢迎指正啊.代码排版有点乱... 本文有以下内容 DNS报文的填充和解析 利用socket API传输信息 一.填充DNS请求报文 随便百度一下,就可以知道DNS报文的格式.所以这里只介绍如何填充DNS报文. 首先是填充报文首部: ? 1 2 3 4 5 6 7 8 9 /* 填充首部的格式大致相同,下面的

Golang网络库中socket阻塞调度源码剖析

本文分析了Golang的socket文件描述符和goroutine阻塞调度的原理.代码中大部分是Go代码,小部分是汇编代码.完整理解本文需要Go语言知识,并且用Golang写过网络程序.更重要的是,需要提前理解goroutine的调度原理. 1. TCP的连接对象: 连接对象: 在net.go中有一个名为Conn的接口,提供了对于连接的读写和其他操作: type Conn interface { Read(b []byte) (n int, err error) Write(b []byte)

OpenFastPath(2):原生态Linux Socket应用如何移植到OpenFastPath上?

版本信息: ODP(Open Data Plane): 1.19.0.2 OFP(Open Fast Path): 3.0.0 1.存在的问题 OpenFastPath作为一个开源的用户态TCP/IP协议栈,其对用户提供的Socket API,无论是宏定义.数据结构还是函数,均以OFP_开头.如下图所示: 1 int ofp_socket(int, int, int); 2 int ofp_socket_vrf(int, int, int, int); 3 int ofp_accept(int,

python Socket编程-python API 与 Linux Socket API之间的关系

python socket编程 服务端 #!/usr/bin/env python # coding=utf-8 from socket import * HOST = '' PORT = 2345 BUFSIZE = 1024 ADDR = (HOST,PORT) #创建AF_INET地址族,TCP的套接字 with socket(AF_INET,SOCK_STREAM) as tcpSerSock: #绑定ip和端口 tcpSerSock.bind(ADDR) #监听端口,是否有请求 tcp