本篇文章目的是为了帮助理解使用socket传输数据时,过程是怎么样的,数据的走向是怎么样的,如有错误多多指正。
socket是计算机之间进行通信的一种约定方式,是应用层和输出层之间的一个抽象层。听过这个抽象层可以接受其他计算机的数据,也可以接收其他计算机的数据。如果这样比较难理解,可以把socket简单的理解成一个文本文件,一台计算机往socket上面写信息,另一台计算机从上面把信息取出来。
在unix系统中,要完成一次数据传递大概的流程如下图所示:
server/client代表服务端/客户端。内测的数字代表了工作的先后顺序,服务端和客户端都创建socket,然后服务端绑定端口并且进入监听状态,当收到客户端的connect信号,用accept接受连接请求。
连接成功后,就可以进行写send()读recv()操作。在传输完成后,关闭两端的socket。

按照上面的流程,第一步,我们先分别在两端创建socket套接字。(1)
使用socket() 函数;
int socket(int af, int type, int protocol);
af是本机IP地址类型,一般有PF_INET或者AF_INET(IPv4互联网协议族),还有PF_INET6(IPv6互联网协议族)等,但是一般用IPv4。
type有两种SOCK_STREAM 和SOCK_DGRAM分别对应tcp和udp协议,区别是用不用建立连接。
protocol是最终使用协议,默认填0就行。
因为socket函数返回值是int类型所以我们设置一个int值来存储。
客户端创建一个socket()如下:
int m_client = socket(AF_INET,SOCK_STREAM,0)
socket函数如果返回值<=0代表创建失败,一般不会出现这种情况。创建成功会返回一个大于零的值sfd,这个值和文件描述符fd很像,所以可以把socket比作一个文件。
之后我们可以通过这个sfd来操作socket。系统有一个数组存储了这个socket,fd就相当于这个数组一个索引,通过这个fd就可以找到并读取这个socket。
然后在客户端也创建一个如上的socket(m_server)后,进入下一步。(2)(3)
假设m_client是3,m_server是4;
接下来是服务端的绑定和监听操作,其实这两部不一定在客户端的connect之后,只是如果没有这两步,客户端也即使发出connect信号也无法连接,所以我们按理想的流程进行。
在服务端中调用bind()和listen()函数。
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
bind()函数中的sock是我们之前创建服务端socket,假设是m_server的话,这里就填m_server。
第二个是sockaddr结构体,里面存放了协议,端口,IP地址等信息:
struct sockaddr {
unsigned short sa_family; // 协议族,与socket()函数的第一个参数相同,填AF_INET。
unsigned char sa_data[14]; // 14字节的端口和地址。
};
所以我们创建一个sockaddr来储存这些信息,假设创建的sockaddr叫servaddr。信息手动赋值,如果麻烦可以用sockaddr_in结构体填写后转换成sockaddr。假设端口是5000,IP是htonl(INADDR_ANY),代表所有IP都能和它通讯,不会屏蔽。协议是AF_INEF。
简单来说,我们要把m_server这个socket与sveraddr里面的端口绑定,就是靠bind来完成的。好了,现在编号为4的socket绑定了端口5000。
第三个参数是sockaddr的大小,填sizeof(servaddr)就行。
int listen(int sock, int backlog);
listen两个参数,第一个继续填m_server,也就是4。第二个填请求队列最大长度,一般填5,10,20都行。(如果需求小的话)
这里干了什么呢?首先listen函数中sock值告诉是m_server的socket,接着就可以知道之前bind的端口5000,所以开始监听5000端口。
第三步,客户端发送connect()请求,服务端accept()接受请求。(4)
首先看客户端,客户端调用connect()函数,
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
这个函数长得和之前的bind差不多,用到的参数意思是一样的,只是要填的不一样,里面实现的功能也不一样。
首先是sock填m_client,这里同样先要new一个sockaddr,端口继续是5000,IP是要连接的服务端的IP地址,这里假设服务端IP是255.255.255.255,协议是AF_INEF。然后继续取名叫servaddr。创建好后填写到第二个参数(填指针地址),第三个参数继续填sizeof(servaddr)。
这里就是往5000端口里发送连接信息。
接着到服务端,服务端调用accept()函数接受连接请求:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
第一个参数填m_server,第二个参数我们要设置新的sockaddr取名caddr,这个sockaddr我们不需要往里面填东西。
注意我们这里要创建一个新的int类型,取名就叫m_client(在服务端里,不会名字冲突),因为accept()会返回一个socket,这个socket其实就是客户端的socket m_client,所以我们在服务端里也直接取同名m_client。
从这里开始,客户端和服务端的联系就开始紧密起来了,从现在开始m_server这个socket就不要了,我们之后的读和写就已经同一在一个地方了,就是m_client,后续的操作也都是对m_client的操作。
第四步,发送信息send()和接受信息recv()。(5)(6)
ssize_t send(int fd, const void *buf, size_t nbytes);
ssize_t recv(int fd, const void *buf, size_t nbytes);
recv()和send()长得一样,只是一个是文件(也可以是数组,字符串等)读入缓冲区;一个是缓冲区读入文件。
缓冲区就是临时存放数据的地址。
假设我们设一个字符串sttr = “abcdefg”
现在要把这个字符串从客户端发送到服务端;
首先,在客户端中我们直接用send()函数直接发送,fd就填m_client,buf就填&sttr,nbtytes就填sttr的大小。这表面我们把sttr里面的数据写入m_client。
然后在服务端这边,我们先要创建一个接收信息的容器,就设char buffer[100];这个buffer用来装接收到的数据。
然后调用recv函数,第一个参数填m_client()。第二个参数填&buffer。第三个参数,因为大多数不知道发送过来多大的数据,所以我们需要先查一下获取数据大小。这里因为客户端和服务端都由我们操作,所以sttr的大小我是知道的。
这一步中recv函数把m_client里面的数据读取到了buffer里面。
到这里,就完成了一个简单的数据传输操作了。
最后一步就是把服务端和客户端的socket都关闭,调用close();(7)