linux就该这么学【网络编程socket套接字】
本节重点认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;学习socket api的基本用法;能够实现一个简单的udp客户端/服务器;能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本);理解tcp服务器建立连接, 发送数据, 断开连接预备知识源IP地址和目的IP地址在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.我们都知道ip是
目录
本节重点
- 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
- 学习socket api的基本用法;
- 能够实现一个简单的udp客户端/服务器;
- 能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本);
- 理解tcp服务器建立连接, 发送数据, 断开连接
预备知识
源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
我们都知道ip是标识着全网的唯一一台主机,只要找到了目的ip地址就可以确定数据该往哪里传输, 而源ip就是数据包发送的起始地。
举例理解:当我们在电脑上双击浏览器之后输入网址,向服务器发送请求,浏览器为了将数据包发送会通过封装(添加协议报头)最后在链路层加上mac帧报头,最后浏览器通过硬件设备网卡向以太网中丢入数据包,而此数据包的报头mac帧地址对应的就是该服务器的mac地址, 所以服务器端就会从以太网中接受对应数据包 , 服务器端的网卡就将读到数据包,去除mac报头,自底向上传输,没经过一层协议就解包,最后将数据交给服务器端,服务器接受数据处理完返回,也需要封装数据包,通过网卡发送给客户端,客户端在通过网卡拿到数据包,自底向上解包,最后应用层拿到数据,而这就是使用套接字传输数据包,浏览器是一个客户端进程,而服务器是服务器端进程,读者可以将该过程看作是一次进程间通信,因为套接字的本质就是进程间通信
重要思考:
我们光有IP地址就可以完成通信了嘛? 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个进程进行解析。
认识端口号,端口号(port)是传输层协议的内容。
- 端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
;IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用:
对应关系是1 :1
结论:
那么有了ip 也有了 port就能找到全网中唯一 一台主机上的进程
总结:
客户端发送请求服务器端收到请求返回处理结果,本质是一次进程间通信,使用socket完成进程间通信是需要跨网络的,在这里网络才是两个进程完成通信的公共资源(我们之间讲过的管道), 区别于只在本地完成进程间通信的方式不需要跨网络,socket方式完成进程间通信涉及的原理更复杂。
理解 “端口号” 和 “进程ID”
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
并不是所有的进程都拥有端口号,而是进程间通信涉及了网络,才会分配给你端口号,本地进程间通信不需要使用端口号,而进程id是只要创建了一个进程就会给你分配一个pid。
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;
认识TCP协议
作用:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
1)源端口和目的端口
各占2个字节,分别写入源端口号和目的端口号,TCP的分用功能是通过端口实现的。
(2)序号
占4个字节。序号范围是[0,232-1],共232(即4282967296)个序号,序号增加到2^32-1后,下一个序号就回到0,也就是说序号使用mod 2^32运算。TCP是面向字节流的。在一个TCP连接中传输的字节流中的每一个字节都是按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节的序号。例如,一报文段的序号字段值是301,而携带的数据共有100字节。这就说明:本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400.显然,下一个报文段(如果还有的话)的数据序号应当从401开始,即下一个报文段的序号字段值应为401.这个字段的名称也叫做“报文段序号”。
(3)确认号
占4个字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501-700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中确认号置为701。请注意,现在的确认号不是500,也不是700,而是701.
总之,应当记住:若确认号=N,则表明:到序号N-1为止的所有数据都已正确收到。
由于序号字段有32位长,可对4GB(即4千兆字节)的数据进行编号。在一般情况下,可保证当序号重复使用时。旧序号的数据早已通过网络到达终点了。
(5)数据偏移
占4位,它指出TCP报文段的数据起始处距离TCP报文段的起始位置有多远。这个字段实际上是指出TCP报文段的首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的。但请注意,“数据偏移”的单位长度是32位字(即以4字节长的字位计算单位)。由于4位二进制数能够表达的最大十进制数为15,因此数据偏移的最大值是60字节,这也是TCP首部的最大长度(即选项长度不能超过40字节)
(6)保留占6位,保留为今后使用,但目前应置为0。下面有6个控制位说明报文段的性质,它们的意义见下面的(7)-(12)。
(7)紧急URG(URGent)
当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不是按原来的排队顺序来传送。例如,已经发送了很长的一个程序要在远地的主机上运行。但是后来发现了一些问题,需要取消该程序的运行。因此用户从键盘发出中断命令(Control+c)。如果不使用紧急数据,那么这两个字符才被交付到接受方的应用进程。这样做就浪费了许多时间。
当URG置为1时,发送应用进程就告诉发送方的TCP有紧急数据要传送。于是发送方TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据,这是要与首部中紧急指针字段配合使用。
(8)确认ACK(ACKnowledgment) 仅当ACK=1是确认号字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置为1
(9)推送PSH(PuSH) 当两个应用进程进行相互交互的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作。这时,发送方TCP把PSH置为1,并立即创建一个报文段发送出去。接收端TCP收到PSH=1的报文段,就尽快地(即“推送”向前)交付给接收应用程序,而不再等到整个缓存都填满了后在向上交付。
虽然应用程序可以选择推送操作,但推送操作还很少使用。
(10)复位RST(ReSeT) 当RST=1时,表明TCP连接中出现较为严重的差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可以称为重建位或重置位。
(11)同步SYN(SYNchronization) 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接报文请求段。对方若是同意建立连接,则应在响应的报文段中使用SYN=1和ACK=1。因此,SYN置为1就表示这是一个连接请求或连接接收报文。
(12)终止FIN(FINis,意思是“完”、“终”) 用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
(13)窗口
占2个字节。窗口值是[0,2^16-1]之间的整数。窗口指的是发送本报文段的一方的接收窗口(而不是自己的发送窗口)。窗口值告诉对方:从本报文段首部中的确定号算起,接收方目前允许对方发送的数据量。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。总之,窗口值作为接收方让发送方设置为其发送窗口的依据。
例如,设确认号是701,窗口字段是1000。这就表明,从701号算起,发送次报文段的一方还有接收1000个字节数据(字节序号是701-1700)的接收缓存空间。
总之,应当记住,窗口字段明确指出了现在允许对方发送的数据量。窗口值是经常在动态变化着。
(14)校验和
占2个字节。校验和字段检验的范围包括首部和数据这两个部分。和UDP用户数据报一样,在计算校验和时,要在TCP报文段的前面加上12字节的伪首部。伪首部的格式与UDP用户数据报的伪首部一样。但应把伪首部第4个字段中的17改为6(TCP的协议号是6),把第5字段中的UDP长度改为TCP长度。接收方收到此报文段后,仍要加上这个伪首部来计算校验和。若使用IPv6,则相应的伪首部也要改变。
(15)紧急指针
占2个字节。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此紧急指针指出来紧急指针的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。
(16)选项
长度可变,最长可达40字节。当没有选项时,TCP的首部长度是20字节。
认识UDP协议
作用:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
用户数据报UDP有两个字段:数据字段和首部字段。首部字段很简单,只有8个字节,有四个字段组成,每个字段的长度都是两个字节。各字段意义如下:
(1)源端口 源端口号。在需要对方回信时选用。不需要时可全用0。
(2)目的端口 目的端口号。这在终点交付报文时必须要使用到。
(3)长度 UDP用户数据报的长度,其最小值是8(仅有首部)。
(4)校验和 校验UDP用户数据报在传输中是否有错,有错就丢弃。
网络字节序
内存地址中的大端和小端
- 大端(存储)模式:是指一个数据的低位字节序的内容放在高地址处,高位字节序存的内容放在低地址处。
- 小端(存储)模式:是指一个数据的低位字节序内容存放在低地址处,高位字节序的内容存放在高地址处。(可以总结为“小小小”即低位、低地址、小端)
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分
. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
网络字节序和主机字节序的转换接口
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#include <arpa/inet.h>
htons():将16位无符号整数从主机字节序转换成网络字节序;
htonl():将32位无符号整数从主机字节序转换成网络字节序;
ntohs():将16位无符号整数从网络字节序转换成主机字节序;
ntohl():将32位无符号整数从网络字节序转换成主机字节序;
inet_addr():若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址,否则为INADDR NONE
inet_ntoa():将一个十进制网络字节序转换为点分十进制IP格式的字符串。
- 这些函数名很好记,
h表示host
,n表示network
,l表示32位长整数,s表示16位短整数。 例如htonl表示将32位的长整数从主机字节序转换为网络字节序
,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
。
注意:网络通信默认是大端存储
socket编程接口
socket 创建 socket 文件描述符
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
函数原型 | int socket(int domain, int type, int protocol); |
---|---|
头文件 | ``#include <sys/types.h> #include <sys/socket.h> |
domain | 指定应用程序使用的通信协议,对于TCP/IP协议族,默认数置为AF_INET |
type | 指定要创建的套接字类型,(字节流(TCP)套接字类型为SOCK_STREAM、数据报(UDP)套接字类型为SOCK_DGRAM);原始套接字SOCK_R AW |
protocol | 指套接字使用的协议,IPPROTO_TCP,IPPROTOUDP,IPPROTO RAW,IPPROTO IP,填入0值表示使用默认的套接字协议 |
返回值 | 如果成功,将返回新套接字的文件描述符 。 出错时,返回-1, errno被适当地设置。 |
加强理解:
- 这里的返回值比较特殊,因为返回的是一个文件描述符,可以设想一下当我们想要完成网络通信,就必须要先打开网卡设备,而网卡是属于硬件,但是网卡是有一个控制他的程序的,而这个文件描述符标识的就是网卡文件的位置,之前我们讲 基础IO的时候是有讲过这块的,就是可以通过文件描述符fd找到struct file* arr[SIZE] 中下标索引为fd所关联的文件。
domain
type
-
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
-
应用程序可以像读写文件一样用read/write在网络上收发数据;
-
对于IPv4, family参数指定为AF_INET;
-
对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
protocol参数的介绍从略,指定为0即可。
UDP
bind绑定端口号
功能:服务端对socket进行地址和端口的绑定
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
函数原型 | int bind(int socket, const struct sockaddr *address,socklen_t address_len); |
---|---|
头文件 | #include <sys/socket.h> #include <arpa/inet.h> |
socket | 即socket描述字 ,它socket()函数创建的唯一标识。 |
address | 一个const struct sockaddress*指针,指向要绑定给sockfd的协议地址 |
address_len | 对应结构体的长度大小 |
返回值 | 成功返回0,错误返回-1。 |
功能: | 将内存文件和网络信息关系起来,服务端对socket进行地址和端口的绑定 |
struct sockaddr{
unsigned short sa_family; // 2 bytes address family,AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
// IPv4 AF_INET sockets:
struct sockaddr_in{
short sin_family; // 2 bytes AF_INET,表示该socket位于Internet域
unsigned short sin_port; // 2 bytes htos() (网络字节顺序)
struct in_addr sin_addr; // 4 bytes inet_addr() 网络字节顺序存储IP地址
char sin_zero[8]; // 8 bytes 填充作用
}
- 这两个结构体都是16个字节,而且都有family属性,不同的是:sockaddr用其余14个字节来表示sa data,而sockaddr_in把14个字节拆分成端口、ip地址。
- sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。sockaddr是给操作系统用的,sockaddr_in区分了地址和端口,使用更方便。
重要:sockaddr结构表示套接字信息
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同
可以通过该命令查看一个头文件的结构体内容 grep -nER 'struct sockaddr_in {' /usr/include/
- IPv4和IPv6的地址格式定义在netinet/in.h中,
IPv4地址用sockaddr_in结构体表示
,包括16位地址类型, 16位端口号和32位IP地址. IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址
,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数
sockaddr 结构
struct sockaddr{
_SOCKADDR_COMNMON (sa_);/*Common data: address family and length.*/
char sa_data[14];/*Address data.*/
}
sockaddr_in 结构
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
/*Internet address.*/
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr; //in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
};
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
结构图:
bind的使用
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//将字符串序列的ip地址转换为网络序列的ip地址
// INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定的地址,或“所有地址”、“任意地址”。
addr.sin_port = htons(8000); //将32位无符号主机字节序转换为网络字节序
if(bind(s,(SOCKADDR*)&addr, sizeof(SOCKADDR)) < 0){ //绑定
cout << "bind() Error:" << WSAGetLastError() << endl;
exit(1); //绑定失败,终止进程
}
-
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
-
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
-
前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
recvfrom接收UDP连接的另一端数据
函数功能:用来接收远程主机经指定的socket 传来的数据, 并把数据存到由参数buf 指向的内存空间
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
函数原型 | ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); |
---|---|
头文件 | #include <sys/types.h> #include <sys/socket.h> |
sockfd | 参数即为客户端的socket描述字 |
buf | 参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据 |
len | 参数指明buf的长度 |
flags | 一般设置为0(0表示阻塞) |
src_addr | 接受对方的地址 |
addrlen | sockaddr的结构长度. |
返回值 | 成功时返回接收到的字节数,错误返回SOCKET_ERROR |
sendto 从UDP连接的另一端发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len,
int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
函数原型 | ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); |
---|---|
返回值 | 成功返回>0成功拷贝至发送缓冲区的字节数(可能小于len),错误返回SOCKET_ERROR |
头文件 | #include <sys/types.h> #include <sys/socket.h> |
sockfd | 数即为客户端的socket描述字 |
buf | 参数应用要发送数据的缓存 |
len | 实际要发送的数据长度 |
flags | 一般设置为0(0表示阻塞) |
dest_addr | 要发向的地址 |
addrlen | 数据发的长度 |
查看使用TCP网络协议执行的进程netstat -nltp
查看使用UDP网络协议执行的进程netstat -nlup
127.0.0.1本机回环采用UDP协议实现客户端和服务器端交互,socket接口应用
server.hpp
#pragma once
#include <iostream>
#include <string>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
using namespace std;
namespace mzt
{
class Udpserverse
{
private:
/* data */
string _ip; // ip
size_t _port; //端口
size_t _socketfd; // socket文件描述符
public:
// 其实127.0.0.1是一个回送地址,指本地机,一般用来测试使用。,默认端口8080
Udpserverse(const string &ip = "127.0.0.1", int port = 8080) : _ip(ip), _port(port) //创建server对象,初始化ip跟端口
{
}
//初始化服务器
void Serverse_Init()
{
//创建套接字
_socketfd = socket(AF_INET, SOCK_DGRAM, 0); // args: 通信协议 / 套接字类型/ 协议/
cout << "sockfd:" << _socketfd << endl;
// 注意: socket的返回值是3,因为文件描述符默认从0开始分配,
//【0,1,2】已经被标准输入、输出、错误流获取了
struct sockaddr_in addr; //定义用户层网络通信的变量,目的是为给内核数据
addr.sin_family = AF_INET; //初始化sockaddr_in变量的前16个字节
addr.sin_port = htons(_port); //从32位字节序列转换为网络字节序列
addr.sin_addr.s_addr = inet_addr(_ip.c_str()); //将字符串风格的ip地址,
//转换为4字节网络序列的ip地址
socklen_t addrsize = sizeof(addr);
//绑定
/* 将用户层的ip和端口号在系统内核中与server端绑定 ,通过_socketfd标识符找到socket文件*/
int rb = bind(_socketfd, (struct sockaddr *)&addr, addrsize);
if (rb < 0)
{
cerr << "绑定失败" << endl;
exit(1);
}
}
//启动服务器
void start()
{
char *buf = new char[100]; // 定义缓冲区
while (1)
{
buf[0] = 0;
struct sockaddr_in end_pointer; //记录是哪个客户端给服务器发送数据,将来服务器还要发回去
socklen_t addrsize = sizeof(end_pointer); //记录实际所读到的sockaddr_in结构体大小
int len = sizeof(buf) - 1; //读取的最大限度
/* 使用UDP协议完成数据的收和发 */
// recvfrom 接受从UDP连接的另外一端的数据
ssize_t ret = recvfrom(_socketfd, buf, len, 0,
(struct sockaddr *)&end_pointer, &addrsize);
if (ret < 0)
{
cerr << "recvfrom error" << endl;
exit(1);
}
else
{
//读取成功,
buf[ret] = 0;
cout << "client #" << buf << endl;
//服务器端回应客户端, 发送信息
// sendto 接受从UDP连接的另外一端的数据
string str = "server #";
str.append(buf);
socklen_t socklen = sizeof(end_pointer);
int ret = sendto(_socketfd, str.c_str(), str.size(),
0, (struct sockaddr *)&end_pointer, socklen);
}
}
delete buf;
}
~Udpserverse()
{
close(_socketfd);
}
};
void test()
{
//创建服务器对象
Udpserverse *ps = new Udpserverse();
ps->Serverse_Init();
ps->start();
delete ps;
}
}
client.hpp
#pragma once
#include <iostream>
#include <string>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class Udpclient
{
private:
/* data */
string _ip; // ip
size_t _port; //端口
size_t _socketfd; // socket文件描述符
public:
Udpclient(const string &ip = "127.0.0.1", int port = 8080) : _ip(ip), _port(port) //创建Udpclient对象,初始化ip跟端口
{
}
void clientInit()
{
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
cout << "sock:" << _socketfd << endl;
}
//启动客户端
void start()
{
string str;
struct sockaddr_in parr;
parr.sin_addr.s_addr = inet_addr(_ip.c_str());
parr.sin_port = htons(_port);
parr.sin_family = AF_INET;
socklen_t parrsize = sizeof(parr);
char *buf = new char[100];
while (1)
{
buf[0] = 0;
cout << "please enter" << endl;
cin >> str;
//客户端通过UDP向服务器发送数据
sendto(_socketfd, str.c_str(), str.length(),
0, (struct sockaddr *)&parr, sizeof(parr));
//客户端通过UDP接收到服务器数据
int ret = recvfrom(_socketfd, buf, sizeof(buf) - 1, 0,
NULL, NULL);
if (ret > 0)
{
buf[ret] = 0;
cout << "server #" << buf << endl;
}
}
}
~Udpclient()
{
close(_socketfd);
}
};
void Udpclientest()
{
Udpclient *pc = new Udpclient();
pc->clientInit();
pc->start();
delete pc;
}
进程入口函数
服务器端
#include "serverse.hpp"
int main()
{
mzt::test();
return 0;
}
客户端
#include "client.hpp"
int main()
{
Udpclientest();
return 0;
}
执行结果:
使用命令行参数,使用ip和端口号对服务器进行绑定
#include "serverse.hpp"
void Usage(char *argv[])
{
cout << "程序名:" << argv[0] << "ip:" << argv[1] << "port:" << argv[2] << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv);
exit(1);
}
mzt::test(argv);
return 0;
}
void test(char *argv[])
{
Udpserverse *ps = new Udpserverse(argv[1], atoi(argv[2]));
ps->Serverse_Init();
ps->start();
delete ps;
}
使用命令行参数,使用ip和端口号对客户端进行绑定
#include "client.hpp"
void Usage(char *argv[])
{
cout << "程序名: " << argv[0] << "ip:" << argv[1] << "端口:" << argv[2] << endl;
}
int main(int argc, char *argv[]) //程序名 + IP + 端口
{
if (argc != 3)
{
Usage(argv);
exit(1);
}
mzt::test1(argv);
return 0;
}
void test1(char *argv[])
{
Udpclient *pc = new Udpclient(argv[1], atoi(argv[2]));
pc->clientInit();
pc->start();
delete pc;
}
启动server服务和client服务,查看ip地址和端口号是否被绑定
程序演示结果:
通过将本地回环ip地址和8080端口号同时绑定到server端和client使用socket接口完成进程间通信
知识梳理:
客户端为什么不需要主动bind? 服务器的端口号和ip可以被绑定吗?
因为服务器是时时在监听有没有客户端的连接,如果服务器不绑定IP和端口的话,客户端上线的时候怎么连到服务器呢,所以服务器要绑定IP和端口,而客户端就不需要了,客户端上线是主动向服务器发出请求的,因为服务器已经绑定了IP和端口,所以客户端上线的就向这个IP和端口发出请求,这时因为客户开始发数据了(发上线请求),系统就给客户端分配一个随机端口,这个端口和客户端的IP会随着上线请求一起发给服务器,服务收到上线请求后就可以从中获起发此请求的客户的IP和端口,接下来服务器就可以利用获起的IP和端口给客户端回应消息了。
总结:
- 客户端可以绑定但是不需要,客户端需要的是服务器端ip和端口号,
- 不可以,服务器的ip和端口号必须是要确定的,不能随意修改,如果修改了,客户端就不能访问了
INADDR_ANY
struct sockaddr_in addr; //定义用户层网络通信的变量,目的是为给内核数据
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; //初始化sockaddr_in变量的前16个字节
addr.sin_port = htons(_port); //从32位字节序列转换为网络字节序列
addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY这是一个宏,
//表示任意的ip
-
将整个结构体清零;
-
设置地址类型为AF_INET;
-
网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
-
端口号为SERV_PORT, 我们定义为9999
练习:翻译词典
当client端向server端发送单词时,client端就只需要等待server返回结果,server端响应client端,将结果处理完,返回对应的结果值给client端。
Udpserverse.hpp
#pragma once
#include <iostream>
#include <string>
#include <assert.h>
#include <vector>
#include <stdlib.h>
#include <unistd.h>
#include <map>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
using namespace std;
namespace mzt
{
class Udpserverse
{
private:
/* data */
size_t _port; //端口
size_t _socketfd; // socket文件描述符
std::map<string, string> _dict;
public:
// 其实127.0.0.1是一个回送地址,指本地机,一般用来测试使用。,默认端口8080
Udpserverse(int port = 8080) : _port(port) //创建server对象,初始化ip跟端口
{
_dict.insert(make_pair("apple", "苹果"));
_dict.insert(make_pair("banana", "香蕉"));
_dict.insert(make_pair("cake", "蛋糕"));
_dict.insert(make_pair("tomato", "西红"));
}
//初始化服务器
void Serverse_Init()
{
//创建套接字
_socketfd = socket(AF_INET, SOCK_DGRAM, 0); // args: 通信协议 / 套接字类型/ 协议/
cout << "sockfd:" << _socketfd << endl;
// 注意: socket的返回值是3,因为文件描述符默认从0开始分配,
//【0,1,2】已经被标准输入、输出、错误流获取了
struct sockaddr_in addr; //定义用户层网络通信的变量,目的是为给内核数据
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; //初始化sockaddr_in变量的前16个字节
addr.sin_port = htons(_port); //从32位字节序列转换为网络字节序列
addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY这是一个宏,
//表示任意的ip
socklen_t addrsize = sizeof(addr);
//绑定
/* 将用户层的ip和端口号在系统内核中与server端绑定 ,通过_socketfd标识符找到socket文件*/
int rb = bind(_socketfd, (struct sockaddr *)&addr, addrsize);
if (rb < 0)
{
cerr << "绑定失败" << endl;
exit(1);
}
}
//启动服务器
void start()
{
char *buf = new char[100]; // 定义缓冲区
while (1)
{
buf[0] = 0;
struct sockaddr_in end_pointer; //记录是哪个客户端给服务器发送数据,将来服务器还要发回去
socklen_t addrsize = sizeof(end_pointer); //记录实际所读到的sockaddr_in结构体大小
int len = sizeof(buf) - 1; //读取的最大限度
/* 使用UDP协议完成数据的收和发 */
// recvfrom 接受从UDP连接的另外一端的数据
ssize_t ret = recvfrom(_socketfd, buf, len, 0,
(struct sockaddr *)&end_pointer, &addrsize);
if (ret < 0)
{
cerr << "recvfrom error" << endl;
exit(1);
}
else
{
vector<string> v;
v.push_back("no find");
buf[ret] = 0;
cout << "client #" << buf << endl;
map<string, string>::iterator it = _dict.find(buf);
if (it != _dict.end())
{
v[0] = it->second;
}
// char tmp[64];
// sprintf(tmp, "%d", end_pointer.sin_port); //将网络字节序转为点十进制的字符串
//发送数据给客户端
//向客户端发送数据
sendto(_socketfd, v[0].c_str(), v[0].size(), 0,
(struct sockaddr *)&end_pointer, sizeof(end_pointer));
}
}
delete buf;
}
~Udpserverse()
{
close(_socketfd);
}
};
void test(char *argv[])
{
Udpserverse *ps = new Udpserverse(atoi(argv[1]));
ps->Serverse_Init();
ps->start();
delete ps;
}
}
Udpclient.hpp
#pragma once
#include <iostream>
#include <string>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
namespace mzt
{
class Udpclient
{
private:
/* data */
string _ip; // ip
size_t _port; //端口
size_t _socketfd; // socket文件描述符
public:
Udpclient(const char *ip = "127.0.0.1", int port = 8080) : _ip(ip), _port(port) //创建Udpclient对象,初始化ip跟端口
{
}
void clientInit()
{
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
cout << "sock:" << _socketfd << endl;
}
//启动客户端
void start()
{
string str;
struct sockaddr_in parr;
parr.sin_addr.s_addr = inet_addr(_ip.c_str());
parr.sin_port = htons(_port);
parr.sin_family = AF_INET;
socklen_t parrsize = sizeof(parr);
char *buf = new char[1000];
while (1)
{
buf[0] = 0;
cout << "please enter" << endl;
cin >> str;
//客户端通过UDP向服务器发送数据
sendto(_socketfd, str.c_str(), str.length(),
0, (struct sockaddr *)&parr, sizeof(parr));
//客户端通过UDP接收到服务器数据
int ret = recvfrom(_socketfd, buf, sizeof(buf) - 1, 0,
NULL, NULL);
if (ret > 0)
{
buf[ret] = 0;
cout << "server #" << buf << endl;
}
}
delete buf;
}
~Udpclient()
{
close(_socketfd);
}
};
void test1(char *argv[])
{
Udpclient *pc = new Udpclient(argv[1], atoi(argv[2]));
pc->clientInit();
pc->start();
delete pc;
}
}
serverse.cc
#include "serverse.hpp"
void Usage(char *argv[])
{
cout << "程序名:" << argv[0] << "port:" << argv[1] << endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv);
exit(1);
}
mzt::test(argv);
return 0;
}
client.cc
#include "client.hpp"
void Usage(char *argv[])
{
cout << "程序名: " << argv[0] << "ip:" << argv[1] << "端口:" << argv[2] << endl;
}
int main(int argc, char *argv[]) //程序名 + IP + 端口
{
if (argc != 3)
{
Usage(argv);
exit(1);
}
mzt::test1(argv);
return 0;
}
TCP
listen()、服务端监听socket套接字
函数原型 | int listen(int sockfd, int backlog); |
---|---|
sockfd | 要监听的socket描述字 |
backlog | 全连接队列的大小、已经完成了三次握手的过程,状态处于ESTABLISHED |
返回值 | 该函数如果调用成功就返回0,否则返回SOCKET_ERROR。可以调用WSAGetLastError()函数获取错误代码 |
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5),
int s = socket(AF_INET, SOCK_DGRAM, 0);
if(listen(s, 30) == SOCKET_ERROR){
cout << "listen error:" << WSAGetLastError() << endl;
return 0;
}
accept() 服务端接受客户端连接请求
函数原型 | int accept(int sockfd, struct sockaddr *addr, socklen_t* addrlen); |
---|---|
sockfd | 要监听的socket描述字 |
addr | 指向struct sockaddr*的指针,用于返回客户端的协议地址 |
addrlen | 参数为协议地址的长度。 |
返回值 | 成功返回由内核自动生成的一个全新的socket,代表与返回客户的TCP连接。失败,则返回INVALID SOCKET。 |
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr 参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)
recv函数:用recv函数从TCP连接的另一端接收数据
函数原型 | int recv(SOCKET s, char FAR* buf, int len, int flags); |
---|---|
s | 参数即为客户端的socket描述字 |
buf | 参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据 |
len | 参数指明buf的长度 |
flags | 一般设置为0 |
返回值 | 成功时返回接收到的字节数,错误返回SOCKET_ERROR,连接关闭返回0 |
char buf[100];
memset(buf, 0, 100);
int ret = recv(s, buf, 100, 0);
cout << ret << "," << buf << endl;
send函数:用send函数从TCP连接的另一端发送数据
函数原型 | int send(IN SOCKET s, IN const char FAR* buf, IN int len, IN int flags); |
---|---|
s | 参数即为客户端的socket描述字 |
buf | 参数应用要发送数据的缓存 |
len | 实际要发送的数据长度 |
flags | 一般设置为0 |
返回值 | 成功返回>0成功拷贝至发送缓冲区的字节数(可能小于len),错误返回SOCKET_ERROR |
char *str = "hello word"
int ret = send(s, str , strlen(str), 0);
cout << ret << endl;
connect连接服务器
函数原型 | int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
---|---|
头文件 | #include <sys/types.h> #include <sys/socket.h> |
sockfd | 描述符 |
addr | 指针指向一个 struct sockaddr的地址 |
addrlen | addr结构体的大小 |
返回值 | 成功返回0,出错返回-1 |
sockaddr_in addr;
//初始化该结构体
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip
int ret = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
if(rert < 0){
// 连接成功
exit(1);
}
else{
//连接失败
//处理
}
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址。
TCP协议实现客户端和服务器端交互,socket接口应用
多进程版本server.hpp
#include <iostream>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
using namespace std;
const int BACKNUM = 5;
namespace mzt
{
class TCPserver
{
private:
int _ip;
int _port; //端口
int _lsock; //监听
public:
TCPserver(int port = 8080) : _port(port), _lsock(-1) //初始化端口
{
}
//初始化
void TCPserverInit()
{
//创建套接字
_lsock = socket(AF_INET, SOCK_STREAM, 0);
if (_lsock < 0)
{
cout << "socket error" << endl;
exit(1);
}
//绑定
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip
//绑定套接字
int s = bind(_lsock, (struct sockaddr *)&addr, sizeof(addr));
if (s < 0) //绑定失败
{
cerr << "bind error" << endl;
exit(2);
}
//监听失败
if (listen(_lsock, BACKNUM) < 0)
{
cerr << "listen error" << endl;
exit(3);
}
}
void start()
{
//启动
sockaddr_in addr;
while (1)
{
socklen_t len = sizeof(addr);
//接受连接请求,由内核创建套接字并返回
int sock = accept(_lsock, (struct sockaddr *)&addr, &len);
if (sock < 0)
{
cout << "accept error" << endl;
continue;
}
string clin_info = inet_ntoa(addr.sin_addr); //将网络ip地址转换字符串
clin_info += ":";
clin_info += ntohs(addr.sin_port);
cout << "get new link...." << clin_info << sock << endl; //的客户端的ip地址和端口号
pid_t id = fork();
if (id == 0) //创建子进程去执行服务器与客户端的交互过程
{
/* 儿子进程 */
if (fork() > 0)
{ //子进程中再创建子进程
exit(0); //终止儿子进程
}
/* 当子进程被终止之后, 孙子进程就开始执行下面的代码了 */
close(_lsock); //孙子子进程只是以儿子进程为模板创建出来的,会继承父进程的所有资源,
//但是孙子进程关心的只是创建的sock能不能使用的问题,并不需要关心_lsock
//所以可以直接关闭,这样一来也会节省资源
service(sock); //服务
close(sock); //孙子进程使用完sock后释放资源
exit(0); //孙子进程终止
}
close(_lsock); //父进程使用完_lsock及时回收节省socket描述符资源
waitpid(id, NULL, 0); //子进程退出后,父进程回收子进程
}
}
void service(int sock)
{
//接受用户数据报
char buf[1024];
while (1)
{
memset(buf, 0, sizeof(buf));
ssize_t s = recv(sock, buf, sizeof(buf) - 1, 0);
if (s > 0)
{
buf[s] = 0;
cout << "client #" << buf << endl;
//给用户发送数据报
send(sock, buf, sizeof(buf), 0);
}
else if (s == 0) //客户端关闭连接,服务器关闭连接
{
cout << "client quit!" << endl;
close(sock); //当客户端关闭连接后,服务器也需要将刚刚创建出来的sock关闭
break;
}
else //接受失败
{
cout << "recv error" << endl;
break;
}
}
close(sock);
}
~TCPserver()
{
close(_lsock);
}
};
void test2(char *argv[])
{
TCPserver *pt = new TCPserver(atoi(argv[1])); //创建服务器对象
pt->TCPserverInit();
pt->start();
delete pt;
}
}
main.cc
#include "server.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
return 1;
}
mzt::test(argv);
return 0;
}
启动服务器,使用telnet命令查看服务器有没有启动
telnet【主机ip】【主机端口号】
如果出现错误提示:可以自行下载,这里只留下centos版本的安装指令
//error
[root@VM-16-4-centos TCP_socket]# telnet 127.0.0.1 8080
bash: telnet: command not found
yum list telnet* 列出telnet相关的安装包
yum install telnet-server 安装telnet服务
yum install telnet.* 安装telnet客户端
telnet端连接到服务器
使用netstat -ntp
可以查看到由telnet指令创建的客户端已经连接到我们的服务器
服务器端与telnet 客户端完成交互
··输出ctrl + ] 再回车``
补充:
-
telnet命令通常用来远程登录。telnet程序是基于TELNET协议的远程登录客户端程序。Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的 能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。
-
但是,telnet因为采用明文传送报文,安全性不好,很多Linux服务器都不开放telnet服务,而改用更安全的ssh方式了。但仍然有很多别的系统可能采用了telnet方式来提供远程登录,因此弄清楚telnet客户端的使用方式仍是很有必要的。
-
telnet命令还可做别的用途,比如确定远程服务的状态,比如确定远程服务器的某个端口是否能访问。
客户端实现
client.hpp
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
using namespace std;
namespace mzt
{
class Clinet
{
private:
int _lsock; //
int _ip; // ip
int _port; //端口
public:
Clinet(int ip, int port) : _ip(ip), _port(port)
{
}
//初始化服务器
void Clien_Init()
{
//创建套接字
_lsock = socket(AF_INET, SOCK_STREAM, 0); //
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(_port); //主机序列转16位的网络字节序列
addr.sin_addr.s_addr = INADDR_ANY; //任意ip
// connet连接服务器
int rc = connect(_lsock, (struct sockaddr *)&addr, sizeof(addr));
if (rc < 0)
{
cout << "连接失败" << endl;
exit(1);
}
//成功
}
//启动服务器
void start()
{
char buf[64];
while (1)
{
memset(buf, 0, sizeof(buf));
buf[0] = 0;
sockaddr_in addr;
cout << "请输入数据:" << endl;
int ret = read(0, buf, sizeof(buf) - 1); //从标准输入中读取
//发送,发送给谁?
if (ret > 0)
{
buf[ret - 1] = 0;
send(_lsock, buf, strlen(buf) , 0);
int s = recv(_lsock, buf, sizeof(buf) - 1, 0);
if (s > 0) //发送的数据为s的长度
{
buf[s] = 0;
cout << "获取的数据:" << buf << endl;
}
}
}
}
~Clinet()
{
close(_lsock);
}
};
void test1(char *argv[])
{
Clinet *pc = new Clinet(atoi(argv[1]), atoi(argv[2]));
pc->Clien_Init();
pc->start();
delete pc;
}
}
client.cc
#include "client.hpp"
int main(int argc, char *argv[])
{
mzt::test(argv);
return 0;
}
线程版本:只需要更换Server.hpp 中的俩个小函数 ServerRoutine 和start
static void *ServerRoutine(void *arg)
{
pthread_detach(pthread_self()); //线程分离后,主线程不需要join新线程
cout << "create new thread ..." << endl;
int *sock = (int *)arg;
service(*sock);
delete sock;
}
void start()
{
//启动
sockaddr_in addr;
while (1)
{
socklen_t len = sizeof(addr);
//接受连接请求,由内核创建套接字并返回
int sock = accept(_lsock, (struct sockaddr *)&addr, &len);
if (sock < 0)
{
cout << "accept error" << endl;
continue;
}
string clin_info = inet_ntoa(addr.sin_addr); //将网络ip地址转换字符串
clin_info += ":";
clin_info += ntohs(addr.sin_port);
cout << "get new link...." << clin_info << sock << endl; //的客户端的ip地址和端口号
//创建线程
pthread_t tid;
int *p = new int(sock); //修正的做法是使用指针记录lock的值
pthread_create(&tid, NULL, ServerRoutine, (void *) p);
}
}
总结与反思:
其实我们的程序是有一定的隐患的,因为线程之间是随机执行的,并不能保证主线程或者是新线程先执行,那么如果新线程被创建出来了后,刚好要去执行任务,而主线程又accept了一次,返回了新的sock,那么就会将原来的sock的值给修改了。修正的做法是提前使用p指针记录sock的值
更多推荐
所有评论(0)