学习笔记之SOCKET网络编程

18 篇文章 0 订阅

1.什么是SOCKET (SOCKET也就是所谓的套接字)
简单的说它是一个文件.它是使用标准Unix文件描述符和其它程序通讯的方式,Unix中的一切就是文件,程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件或者什么其它的东西.
这里的SOCKET就是一个特定的文件描术符,(称之为Internet 套接字)用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过它向网络发出请求或者应答网络请求.

Internet 套接字分为多种形式,常见的有stream sockets流格式和datagram sockets数据报格式.等.但经常用的就是这两种.数据报套接节有时候也叫无连接套接字.
流式套接字.
流格式的套接字就是有连接的套接字.它是可以双向通讯的数据流.如果你向流格式的套接字按顺序输出"1,2",那么它们将按顺序"1,2"到达另一边.
比如常见的telnet程序就是使用的流式套接字.还有WWW浏览器使用的 HTTP协议也使用它们来下载页面,实际上,当你通过端口80 telnet 到一个 WWW 站点,然后输入 "GET pagename" 的时候,你也可以得到HTML页面的内容
流式套接字使用了一种叫做"TCP"的协议来控制你的数据按顺序到达并不会有错.
我们常说的"TCP/IP"其实是两个东西,它表示"TCP协议"和"Internet 协议"
Internet 协议只是处理Internet 路由而已"
数据报套接字.
数据报套接字也叫无连接套节字.假设你发送一个数据报,它可能会到达,它也可能次数颠倒了.如果它到达,那么在这个包的内部是无错误的.对于数据报套接字,你只需要建立一个包.并构造一个有目标信息的IP头.然后把这个包发送出去.所以说它是无连接的.
数据报套接字使用的是用户数据协议也叫做"UDP"协议来按据数据的传输.当然它们也是用"Internet 协议"来处理Internet 路由的.
使用数据报套接字的程序有tftp, bootp等等.当然在这里你也许会问"如果数据包在传送过程中丢失,程序将如何正常工作?" 每个程序在UDP上有自己的协议.协议每发出一个数据包,收到者必须发回一个包来告诉发送者"我收到了".如果一定时间内发送者没有收到应答,它将重新发送.直到收到应答为止.

2.关于网络分层模型.
首先谈谈数据是如何在网络中以包的方式来传送的.假设现有一台机器上的TFTP程序和另一台机器上的程序通讯.我们知道TFTP程序是使用数据报套接字的.原始数据最先是被第一层协议(这里也就是TFTP)在它的报头包装好,然后整个数据(包括TFTP头)被另一层协议(在这里就是UDP)包装.然后再下一个(IP协议)包装这样一直下去直到物理硬件层(比如以太网卡)当另外一台机器收到数据包.硬件层先剥去以太网头,内核剥去IP和UDP头,最后由TFTP程序剥去TFTP头.就得到了原始数据.
这一过程对应一个网络分层模型.这种模型在描述网络系统上相对于其它模型有很多优点.例如:你可以写一个套接字程序而不用关心数据的物理传输.因为底层的程序会为你处理它们.

网络分层模型:

应用层 (Application)
表示层 (Presentation)
会话层 (Session)
传输层(Transport)
网络层(Network)
数据链路层(Data Link)
物理层(Physical)

如果把它对应到 Unix,结果是:

应用层(Application Layer) (telnet, ftp,等等)
传输层(Host-to-Host Transport Layer) (TCP, UDP)
Internet层(Internet Layer) (IP和路由)
网络访问层 (Network Access Layer) (网络层,数据链路层和物理层)

看了上面的内容就知道建立一个简单的数据包其实是一个非常复杂的工作.但是对于用户而言我们只需要简单的调用send()或sendto()就可以了.内核将为你建立传输层和Internet层,硬件完成网络访问层.

3.SOCKET编程中几个重要的数据结构.
首先是sockaddr它的定义:
struct sockaddr
{
   unsigned short sa_family;  /* 地址家族, AF_xxx */
   char sa_data[14];          /*14字节协议地址*/
};
sa_family 能够是各种各样的类型,但是在这篇文章中都是 "AF_INET".
sa_data包含套接字中的目标地址和端口信息.

为了处理sockaddr程序员创造一个并列的结构sockaddr_in后面的in表示"Internet"它的定义如下:
struct sockaddr_in
{
   short int sin_family;        /* 通信类型 */
   unsigned short int sin_port; /* 端口 */
   struct in_addr sin_addr;     /* Internet 地址 (也就是像"192.168.0.1"这样的IP地址)*/
   unsigned char sin_zero[8];   /* 使其与sockaddr结构的长度相同*/
};

用sockaddr_in结构可以轻松的处理套接字地址的基本元素.注意:sin_zero的作用是为了与结构体sockaddr保持相同的长度.在使用的时候应用memset()类型的函数来将它全部置0.正是有了它.你才可以在使用sockaddr的地方仍然使用sockaddr_in结构代替.只需要做简单的转换即可.
同时注意sockaddr_in中的sin_family和sockaddr中的sa_family一致.
最后sin_port和sin_addr必须是网络字节顺序 (Network Byte Order)

4.关于字节顺序.
事实上有两种字节排列顺序:A.重要的字节在前.B.不重要的字节在前面.
而前一种字节排列顺序就叫网络字节顺序.
当我们说某一数据必须按照网络字节顺序.那么你就需要调用函数(例如:htons())来将它从本机字节顺序转换成网络字节顺序.我们能够转换两种类型"short"和"long"对于unsigned也实用
假如:你想将short从本机字节顺序转换成网络字节顺序.用"h"表示本机(host),接着是"to"然后用"n"表示网络(network)最后用"s"表示short类型,这样就可以得到这个函数htons()...该类型的函数有:
    htons()  "Host to Network Short" //将short类型由本机字节顺序转换成网络字节顺序
  htonl()  "Host to Network Long"  //将long类型由本机字节顺序转换成网络字节顺序
  ntohs()  "Network to Host Short" //将short类型由网络字节顺序转换成本机字节顺序
  ntohl()  "Network to Host Long"  //将long类型由网络字节顺序转换成本机字节顺序

记住:这里是Unix的世界.在你将数据放到网络上的时候,请确信它们是按网络字节顺序排列的.

为什么在sockaddr_in结构中sin_addr和sin_port需要转换成网络字节顺序.而sin_family不需要呢?
答案是:sin_addr和sin_port分别封装在包的IP层和UDP层.因此它必须按网络字节顺序.但是sin_family只是被内核用来判断数据结构中包含什么类型的地址.它并没有被放到网络上进行传送.所以它可以是本机字节顺序.

5.IP地址及其处理方法
假设你现在已经创建了一个sockaddr_in结构体myaddr 现在你要将IP地址"192.168.1.110"存贮到结构体中.你只需要调用函数inet_addr()将IP地址从点数格式转换成无符号长整型.例:
myaddr.sin_addr.s_addr = inet_addr("192.168.1.110");
需要注意的是:函数inet_addr()返回的已经是网络字节顺序.所以你无需再调用htonl()函数来进行转换.
还需要注意一点:函数inet_addr()在错误时返回值为-1.刚好等于IP地址"255.255.255.255"这可是个广播地址.所以在使用inet_addr()函数时一定要进行错误检查.

函数inet_ntoa()则刚好相反,它可以将一个结构体in_addr输出成点数格式的IP地址.
注意:inet_ntoa()函数将结构体in_addr作为一个参数.而不是长整型.

6.socket()函数
功能:建立一个套接字
定义:int socket(int domain, int type, int protocol);
参数domain指定通信发生的区域,windows中只支持"AF_INET"
参数type描述要建立的套接字的类型.如SOCK_STREAM类型或SOCK_DGRAM类型
参数protocol说明该套接字使用的特定协议.如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式

函数socket()将会根据指定的三个参数建立一个套接字.并将相应的资源分配给它.同时返回一个整型套接字描述符.
注意:如果该函数调用失败返回值为-1.

7.bind()函数
功能:将套节字和本地地址关联在一起
定义:int bind(SOCKET s,struct sockaddr *name, int namelen);
参数s是由socket()函数返回的并且未作连接的套接字描述符
参数name是赋给套接字s的本地地址结构的指针.(也就是sockaddr_in结构)
参数namelen表明了本地地址结构的长度.

函数调用成功返回0,有错误则返回-1.

在进行绑定本地地址时有几个注意事项:
A.将sockaddr_in结构的sin_port置为0时,告诉系统随机选择一个没有使用的端口.
B.将sockaddr_in结构的sin_addr.saddr置为INADDR_ANY,告诉它自动填上它所运行的机器的IP地址
C.如果需要手动指定端口.一定记住不要采用小于1024的端口,因为所有小于1024的端口都被系统保留.而我们可以选择的端口则从1024到65535
D.如果你使用connect()来和远程机器通讯,你不需要关心你的本地端口,你只需要简单的调用connect()就可以了,它会检查套接字是否绑字端口,如果没有,它会自己绑定一个没有使用的端口.

8.connect()函数
定义:int connect(SOCKET s,struct sockaddr *name, int namelen);
参数s是欲建立连接的本地套接字描述符.
参数name是将要连接的目标地址结构sockaddr的指针.
参数namelen指明目标地址结构的长度

函数在错误时返回-1.
注意:在使用connect()函数来和远程机器建立连接时可以不需要调用bind()函数,因为我在乎本地端口号.我只关心我要连接到哪里(也就是目标),并且连接后目标会自动获取我们的IP地址,以及我们所使用的端口等信息.

9.listen()和accept()函数.
假如你不希望与远程的一个地址相连,那么你就需要等待接入请求.并且用各种方法处理它们.这个过程就是听及listen()然后接受accept()

listen()用于监听连接.它地调用需在accept()之前
定义:int listen(SOCKET s, int backlog);
参数s标识一个本地已建立,尚未连接的套接字描述符.
参数backlog表示请求连接队列的最大长度,大多数系统允许最大数目是20.
成功返回0,错误则返回-1.

accept()函数
系统调用accept()的情况就像有一台远程的机器通过你在监听listen()的端口连接connect()到你的机器.这个连接将被加入到等待接受的队列中.然后你调用accept()告诉它你有空闲的连接.这时系统将返回一个新的套接字描术符.这样你就有两个套接字了.原来的一个还在监听那个端口是否有新的连接到来.新的这个套接字则准备发送send()或者接收recv()数据了.
定义:SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
参数s为本地套接字描述符,也就是正在用于监听listen()的那个套接字描术符.
参数addr是一个struct sockaddr结构指针.它用来存贮客户的地址.也就是要求连接的机器的地址信息.
参数addrlen保存客户方套接字地址的长度(注意它是一个int型指针)
函数调用成功将会返回一个套接字(表示接收到的套接字).如果失败则返回-1.

10.send()和recv()函数
不管是服务端程序或是客户端程序都将使用这两个函数从TCP连接的另一端发送或接收数据.

send()函数
定义:int send(int s, void *msg, int len, int flags);
参数s是你相发送数据的套接字描述符.这里可能是socket()返回的,也可能是accept()返回的.
参数msg是你想要发送的数据的指针.
参数len是将要发送的数据的长度.
参数flags通常设为0.

如果将要发送的数据的长度len大于套接字s的发送缓冲区的长度.该函数将返回-1.
如果len小于或等于s的发送缓冲区长度,那么send()先检果协议是否正在发送s的发送缓冲区中的数据.如果是就等待协议把数据发送完.如果协议还没有开始发送s的发送缓冲区中的数据或者s的发送缓冲区中没有数据,那么send()就比较发送缓冲区和剩余空间和len,如果len大于s的发送缓冲区剩余空间send()就会一直等待协议把s的发送缓冲区中的数据发送完.如果len小于发送缓冲区的剩余空间send()就把msg所指的数据拷贝到s的发送缓冲区剩余空间里.

注意:并不是send()把数据传送到连接的另一端.而是具体的协议来完成传送的.send()只是把数据拷贝到套接字的发送缓冲区剩余空间里而以.
如果send()拷贝数据成功.就返回实际拷贝数据的字节数.
另外send()把数据拷贝到套接字的发送缓冲区中就返回了.此时.这些数据并不一定马上被传送到连接的另一端.

注意:在Unix系统下.如果send()在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止.

recv()函数
定义:int recv(int s, void *buf, int len, int flags);
参数s为接收端的套接字描述符.
参数buf指明一个缓冲区.该缓冲区用来存放recv()接收到的数据.
参数len指明缓冲区的长度.
参数flags通常设为0.

成功将返回实际写入缓冲区的数据的长度.失败返回-1.
同样的.recv()函数只是负责把数据从s的接收缓冲区中拷贝到buf缓冲区中.真正的接收数据的工作是由协议来完成的.所以协议接收到的数据有可能大于buf缓冲区的长度.在这种情况下要调用多次recv()函数才能把接收缓冲区中的数据拷贝完.

另外:除了send()函数外其它的socket函数比如recv()都要先等待套接字的发送缓冲中的数据被协议传送完毕才能执行.


今天就到此吧!!!

自:http://blog.sina.com.cn/s/blog_56ea069101000bx5.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值