socket编程(C++)
介绍
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
过程介绍
服务器端和客户端通信过程如下所示:
![socket通信过程](http://images.cnblogs.com/cnblogs_com/helloworldcode/1414395/o_05232335-fb19fc7527e944d4845ef40831da4ec2.png)
服务端
服务端的过程主要在该图的左侧部分,下面对上图的每一步进行详细的介绍。
1. 套接字对象的创建
/*
* _domain 套接字使用的协议族信息
* _type 套接字的传输类型
* __protocol 通信协议
* */
int socket (int __domain, int __type, int __protocol) __THROW;
socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,可以将该函数类比常用的open()
函数,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
参数介绍
第一个参数:关于协议族信息可选字段如下,只列出一般常见的字段。
地址族 | 含义 |
---|---|
AF_INET | IPv4网络协议中采用的地址族 |
AF_INET6 | IPv6网络协议中采用的地址族 |
AF_LOCAL | 本地通信中采用的UNIX协议的地址族(用的少) |
第二个参数:套接字类型。常用的有SOCKET_RAW,SOCK_STREAM和SOCK_DGRAM。
套接字类型 | 含义 |
---|---|
SOCKET_RAW | 原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议。 |
SOCK_STREAM | SOCK_STREAM是数据流,一般为TCP/IP协议的编程。 |
SOCK_DGRAM | SOCK_DGRAM是数据报,一般为UDP协议的网络编程; |
第三个参数:最终采用的协议。常见的协议有IPPROTO_TCP、IPPTOTO_UDP。如果第二个参数选择了SOCK_STREAM,那么采用的协议就只能是IPPROTO_TCP;如果第二个参数选择的是SOCK_DGRAM,则采用的协议就只能是IPPTOTO_UDP。
2. 向套接字分配网络地址——bind()
/*
* __fd:socket描述字,也就是socket引用
* myaddr:要绑定给sockfd的协议地址
* __len:地址的长度
*/
int bind (int __fd, const struct sockaddr* myaddr, socklen_t __len) __THROW;
第一个参数:socket文件描述符__fd
即套接字创建时返回的对象,
第二个参数:myaddr
则是填充了一些网络地址信息,包含通信所需要的相关信息,其结构体具体如下:
struct sockaddr
{
sa_family_t sin_family; /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
在具体传参的时候,会用该结构体的变体sockaddr_in
形式去初始化相关字段,该结构体具体形式如下,结构体sockaddr
中的sa_data
就保存着地址信息需要的IP地址和端口号,对应着结构体sockaddr_in
的sin_port
和sin_addr
字段。
struct sockaddr_in{
sa_family_t sin_family; //前面介绍的地址族
uint16_t sin_port; //16位的TCP/UDP端口号
struct in_addr sin_addr; //32位的IP地址
char sin_zero[8]; //不使用
}
in_addr
结构定义如下:
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
而sin_zero
无特殊的含义,只是为了与下面介绍的sockaddr结构体一致而插入的成员。因为在给套接字分配网络地址的时候会调用bind
函数,其中的参数会把sockaddr_in
转化为sockaddr
的形式,如下:
struct sockaddr_in serv_addr;
...
bind(serv_socket, (struct sockaddr*)&serv_addr, sizeof(serv_addr);
需要注意的是s_addr
是一种uint32_t
类型的数据,而且在网络传输时,统一都是以大端序的网络字节序方式传输数据,而我们通常习惯的IP地址格式是点分十进制,例如:“219.228.148.169”,这个时候就会调用以下函数进行转化,将IP地址转化为32位的整数形数据,同时进行网络字节转换:
in_addr_t inet_addr (const char *__cp) __THROW;
//或者
int inet_aton (const char *__cp, struct in_addr *__inp) __THROW; //windows无此函数
如果单纯要进行网络字节序地址的转换,可以采用如下函数:
/*Functions to convert between host and network byte order.
Please note that these functions normally take `unsigned long int' or
`unsigned short int' values as arguments and also return them. But
this was a short-sighted decision since on different systems the types
may have different representations but the values are always the same. */
// h代表主机字节序
// n代表网络字节序
// s代表short(4字节)
// l代表long(8字节)
extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__));
extern uint16_t ntohs (uint16_t __netshort)
__THROW __attribute__ ((__const__));
extern uint32_t htonl (uint32_t __hostlong)
__THROW __attribute__ ((__const__));
extern uint16_t htons (uint16_t __hostshort)
3. 进入等待连接请求状态
给套接字分配了所需的信息后,就可以调用listen()
函数对来自客户端的连接请求进行监听(客户端此时要调用connect()
函数进行连接)
/* Prepare to accept connections on socket FD.
N connection requests will be queued before further requests are refused.
Returns 0 on success, -1 for errors. */
extern int listen (int __fd, int __n) __THROW;
第一个参数:socket文件描述符__fd
,分配所需的信息后的套接字。
第二个参数:连接请求的队列长度,如果为6,表示队列中最多同时有6个连接请求。
这个函数的fd(socket套接字对象)就相当于一个门卫,对连接请求做处理,决定是否把连接请求放入到server端维护的一个队列中去。
4. 受理客户端的连接请求
listen()
中的sock(__fd : socket对象)发挥了服务器端接受请求的门卫作用,此时为了按序受理请求,给客户端做相应的回馈,连接到发起请求的客户端,此时就需要再次创建另一个套接字,该套接字可以用以下函数创建:
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (int __fd, struct sockaddr *addr, socklen_t *addr_len);
函数成功执行时返回socket文件描述符,失败时返回-1。
第一个参数:socket文件描述符__fd
,要注意的是这个套接字文件描述符与前面几步的套接字文件描述符不同。
第二个参数:保存发起连接的客户端的地址信息。
第三个参数: 保存该结构体的长度。
5. send/write发送信息
linux下的发送函数为:
/* Write N bytes of BUF to FD. Return the number written, or -1.
This function is a cancellation point and therefore not marked with
__THROW. */
ssize_t write (int __fd, const void *__buf, size_t __n) ;
而在windows下的发送函数为:
ssize_t send (int sockfd, const void *buf, size_t nbytes, int flag) ;
第四个参数是传输数据时可指定的信息,一般设置为0。
6. recv/read接受信息
linux下的接收函数为
/* Read NBYTES into BUF from FD. Return the
number read, -1 for errors or 0 for EOF.
This function is a cancellation point and therefore not marked with
__THROW. */
ssize_t read (int __fd, void *__buf, size_t __nbytes);
而在windows下的接收函数为
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flag) ;
7. 关闭连接
/* Close the file descriptor FD.
This function is a cancellation point and therefore not marked with
__THROW. */
int close (int __fd);
退出连接,此时要注意的是:调用close()
函数即表示向对方发送了EOF
结束标志信息。
客户端
服务端的socket套接字在绑定自身的IP即 及端口号后这些信息后,就开始监听端口等待客户端的连接请求,此时客户端在创建套接字后就可以按照如下步骤与server端通信,创建套接字的过程不再重复了。
1. 请求连接
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
For connectionless socket types, just set the default address to send to
and the only address from which to accept transmissions.
Return 0 on success, -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
int connect (int socket, struct sockaddr* servaddr, socklen_t addrlen);
几个参数的意义和前面的accept函数意义一样。要注意的是服务器端收到连接请求的时候并不是马上调用accept()函数,而是把它放入到请求信息的等待队列中。
套接字的多种可选项
可以通过如下函数对套接字可选项的参数进行获取以及设置。
/* Put the current value for socket FD's option OPTNAME at protocol level LEVEL
into OPTVAL (which is *OPTLEN bytes long), and set *OPTLEN to the value's
actual length. Returns 0 on success, -1 for errors. */
extern int getsockopt (int sock, int __level, int __optname,
void *__optval, socklen_t *optlen) __THROW;
/* Set socket FD's option OPTNAME at protocol level LEVEL
to *OPTVAL (which is OPTLEN bytes long).
Returns 0 on success, -1 for errors. */
extern int setsockopt (int sock, int __level, int __optname,
const void *__optval, socklen_t __optlen) __THROW;
参数说明:
scok: 套接字的文件描述符
** __level ** :可选项的协议层,如下:
协议层 | 功能 |
---|---|
SOL_SOCKET | 套接字相关通用可选项的设置 |
IPPROTO_IP | 在IP层设置套接字的相关属性 |
IPPROTO_TCP | 在TCP层设置套接字相关属性 |
** __optname ** :要查看的可选项名,几个主要的选项如下
选项名 | 说明 | 数据类型 | 所属协议层 |
---|---|---|---|
SO_RCVBUF | 接收缓冲区大小 | int | SOL_SOCKET |
SO_SNDBUF | 发送缓冲区大小 | int | SOL_SOCKET |
SO_RCVLOWAT | 接收缓冲区下限 | int | SOL_SOCKET |
SO_SNDLOWAT | 发送缓冲区下限 | int | SOL_SOCKET |
SO_TYPE | 获得套接字类型(这个只能获取,不能设置) | int | SOL_SOCKET |
SO_REUSEADDR | 是否启用地址再分配,主要原理是操作关闭套接字的Time-wait时间等待的开启和关闭 | int | SOL_SOCKET |
IP_HDRINCL | 在数据包中包含IP首部 | int | IPPROTO_IP |
IP_MULTICAST_TTL | 生存时间(Time To Live),组播传送距离 | int | IPPROTO_IP |
IP_ADD_MEMBERSHIP | 加入组播 | int | IPPROTO_IP |
IP_OPTINOS | IP首部选项 | int | IPPROTO_IP |
TCP_NODELAY | 不使用Nagle算法 | int | IPPROTO_TCP |
TCP_KEEPALIVE | TCP保活机制开启下,设置保活包空闲发送时间间隔 | int | IPPROTO_TCP |
TCP_KEEPINTVL | TCP保活机制开启下,设置保活包无响应情况下重发时间间隔 | int | IPPROTO_TCP |
TCP_KEEPCNT | TCP保活机制开启下,设置保活包无响应情况下重复发送次数 | int | IPPROTO_TCP |
TCP_MAXSEG | TCP最大数据段的大小 | int | IPPROTO_TCP |
** __optval ** :保存查看(get)/更改(set)的结果
** optlen ** : 传递第四个参数的字节大小
这里只对几个可选项参数进行说明:
1.设置可选项的IO缓冲区大小
参考案例如下:
int status, snd_buf;
socklen_t len = sizeof(snd_buf);
status = getsockopt(serv_socket, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
cout << "发送缓冲区大小: " << snd_buf <<endl;
虽然可以获得的接收/发送缓冲区的大小,但是通过设置接收/发送缓冲区大小时,得到的结果会与我们期望的不一样,因为对缓冲区大小的设置是一件很谨慎的事,其自身会根据设置的值进行一定的优化。
2. 是否启用地址再分配与Time-wait时间等待
关于地址再分配问题会发生在这种情况下,首先看两种情况,假设客户端和服务器正在通讯(测试代码下载地址)。
① 在客户端的终端按Crtl + c
或者其他方式断开与服务器的连接,此时客户端发送FIN消息,经过四次握手断开连接,操作系统关闭套接字,相当于close()
的过程。然后在次启动客户端,顺利启动。
② 在服务端的终端按Crtl + c
或者其他方式断开与客户端的连接,像①中一样,再次启动服务端,此时出现bind() error
错误。
服务器端出现这种情况的原因是调用套接字分配网络地址函数bind()
的时候之前使用建立连接的同一端口号还没有来得及停用(大约要过两三分钟才处于可用状态),而客户端申请连接的端口是任意指定,程序运行时会动态分配端口号。
服务器端端口没有被释放到被释放的时间状态称为Time-wait状态,这个状态的出现可以借助TCP断开连接的四次握手协议来分析,如下图:
当client端发送ACK=1 ack=k+1
这个消息给服务端就立即消除套接字,若此时该消息中途传输被遗失,则这个时候server端就永远无法收到client的ACK
消息了。
3. TCP_NODELAY
TCP套接字默认是使用Nagle算法的,该算法的特征是只有收到前一条数据的ACK消息后,才会发送下一条数据。
从网上找到一张图说明使用和禁用Nagle算法的区别(图片来源),如下:
设置代码如下:
#include <netinet/tcp.h> //注意要引入这个头文件
int opt_val = 1;
setsockopt(serv_socket, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, sizeof(opt_val));
程序案例
案例的过程,在网上看到了关于read和write的发送与接受过程的图,便于理解:
![](http://images.cnblogs.com/cnblogs_com/helloworldcode/1414395/o_TCP-socket.jpg)
代码链接 github
注意以上代码都是在ubuntu下运行的,在windows的代码与此有所不同。比如要引入一个<winsock2.h>
的头文件,调用WSAStartup(...)
函数进行Winsock的初始化,而且它们的接受与发送函数也有所不同。
在我的github上有几个简单的demo,可供学习
参考文献
《TCP/IP网络编程》尹圣雨
socket编程(C++)的更多相关文章
- Linux下的C Socket编程 -- server端的继续研究
Linux下的C Socket编程(四) 延长server的生命周期 在前面的一个个例子中,server在处理完一个连接后便会立即结束掉自己,然而这种server并不科学啊,server应该是能够一直 ...
- java socket编程(li)
一.网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输.在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以 ...
- Python Socket 编程——聊天室示例程序
上一篇 我们学习了简单的 Python TCP Socket 编程,通过分别写服务端和客户端的代码了解基本的 Python Socket 编程模型.本文再通过一个例子来加强一下对 Socket 编程的 ...
- Linux下的C Socket编程 -- server端的简单示例
Linux下的C Socket编程(三) server端的简单示例 经过前面的client端的学习,我们已经知道了如何创建socket,所以接下来就是去绑定他到具体的一个端口上面去. 绑定socket ...
- Linux下的C Socket编程 -- 获取对方IP地址
Linux下的C Socket编程(二) 获取域名对应的IP地址 经过上面的讨论,如果我们想要连接到远程的服务器,我们需要知道对方的IP地址,系统函数gethostbyname便能够实现这个目的.它能 ...
- Linux下的C Socket编程 -- 简介与client端的处理
Linux下的C Socket编程(一) 介绍 Socket是进程间通信的方式之一,是进程间的通信.这里说的进程并不一定是在同一台机器上也有可能是通过网络连接的不同机器上.只要他们之间建立起了sock ...
- python网络编程-socket编程
一.服务端和客户端 BS架构 (腾讯通软件:server+client) CS架构 (web网站) C/S架构与socket的关系: 我们学习socket就是为了完成C/S架构的开发 二.OSI七层 ...
- Socket编程实践(2) Socket API 与 简单例程
在本篇文章中,先介绍一下Socket编程的一些API,然后利用这些API实现一个客户端-服务器模型的一个简单通信例程.该例子中,服务器接收到客户端的信息后,将信息重新发送给客户端. socket()函 ...
- Socket编程实践(1) 基本概念
1. 什么是socket socket可以看成是用户进程与内核网络协议栈的编程接口.TCP/IP协议的底层部分已经被内核实现了,而应用层是用户需要实现的,这部分程序工作在用户空间.用户空间的程序需要通 ...
- [转]C语言SOCKET编程指南
1.介绍 Socket编程让你沮丧吗?从man pages中很难得到有用的信息吗?你想跟上时代去编Internet相关的程序,但是为你在调用 connect() 前的bind() 的结构而不知所措?等 ...
随机推荐
- @RequestParam与@PathVariable
@PathVariable 带占位符的 URL 是 Spring3.0 新增的功能,该功能在SpringMVC 向 REST 目标挺进发展过程中具有里程碑的意义 通过 @PathVariable 可以 ...
- 2019年华南理工校赛(春季赛)--L--剪刀石头布(签到)
#include <iostream> using namespace std; int main(){ string a,b,c,d; a="Scissors"; b ...
- html或者jsp页面刷新问题
setTimeout(function(){window.location.reload();//刷新当前页面.},2000) window.location.reload();//刷新当前页面.pa ...
- Jupyter-NoteBook-你应该知道的N个小技巧
智能决策上手系列教程索引 不断更新部分内容来自于翻译整理 多行输出 在Notebook的中开头cell中添加以下代码可以实现多行输出: from IPython.core.interactiveshe ...
- memcache启动报错:memcached: error while loading shared libraries: libevent-XXXXX5: cannot 。。。。
创建连接 ln -s /usr/lib/libevent-2.1.so.6 /usr/lib/libevent-2.1.so.6 如果还不行就下面解决 执行下面语句查看链接地址 LD_DEBUG=l ...
- ubuntu系统用docker搭建wordpress
目标:在docker中搭建wordpress 安装顺序: 首先要有一个云服务器---购买或者自己搭建(本人是自己在主机上装了虚拟机,搭建了一个ubuntu14.04,安装链接:https://www. ...
- win32控制台程序 宽字符与短字符转化
由于vs各版本之间存在字符设置不兼容问题,特总结char与tchar的互相转换函数,如下,在之后的工程中可以使用. void TcharToChar(const TCHAR * tchar, char ...
- 音视频编解码——LAME
一.LAME简介 LAME是目前非常优秀的一种MP3编码引擎,在业界,转码成Mp3格式的音频文件时,最常用的就是LAME库.当达到320Kbit/s时,LAME编码出来的音频质量几乎可以和CD的音质相 ...
- Javascript高级编程学习笔记(71)—— 模拟事件(1)DOM事件模拟
事件,指的是网页中某个特定的交互时刻 一般来说事件由浏览器厂商负责提供,一般由用户操作或者其它浏览器功能来触发 但是有一类特殊的事件,那就是由我们开发人员通过JS触发的事件 这些事件和浏览器创建的事件 ...
- SpringMVC框架一:搭建测试
这里做一个Demo:展示商品列表 新建Dynamic Web Project: 导入jar包,放在lib下: 放入Lib文件夹之后,会自动build path 接下来配置web.xml: <?x ...