学会在linux下的socket 编程

 

学习socket 编程之前必须先要理解TCP/IP通信的过程

 

网络通信的实质:

1解决不同主机进程间的通信.

2首要解决网络间进程标识问题

3、解决多重协议的识别问题

 

OSI TCP/IP 的联系

        OSI                                                                        TCP/IP

osi与TCP/IP的模型

 TCP/IP 通信

 

链路层是设备到设备之间的通信

网络层主机到主机的通信

运输层是进程到进程的通信

应用层是应用程序间的通信

主机间进行通信

 

TCP/IP 各层的对应协议

TCP/IP 各层的对应协议

TCP/IP协议族

应用层:应用程序间沟通的层

FTP、Telnet etc.

传输层:提供进程间的数据传送服务

这一层负责传送数据,并且确定数据已被送达并接收。 

传输控制协议(TCP),用户数据报协议(UDP)

网络层:提供基本的数据封包传送功能

让每个数据包都能够到达目的主机

IP协议(网际协议)

物理链路层:对实际的网络媒体的管理,定义如何使用实际网络来传送数据

 

相关的几种协议间的比较:

IP协议

1、网际协议IP是TCP/IP的心脏,也是网络层中最重要的协议

2、IP层接收由更低层(网络接口层例如以太网设备驱动程序)发来的数据包,并把该数据包发送到更高层---TCP或UDP层;

3、相应的,IP层也把从TCP或UDP层接收来的数据包传送到更低层。

4、IP数据包是不可靠的,因为IP并没有做任何事情来确认数据包是按顺序发送的或者没有被破坏

5、IP数据包中含有发送它的主机的地址(源地址)和接收它的主机的地址(目的地址)。 

 

ICMP协议

1、ICMP与IP位于同一层,被用来传送IP的控制信息

2、主要用来提供有关通向目的地址的路径信息

3、ICMP的‘Redirect’信息通知主机通向其他系统的更准确的路径

4、而‘Unreachable’信息则指出路径有问题

5、PING是最常用的基于ICMP的服务

 

TCP协议

1、TCP协议可以对包进行排序并检查错误,同时实现虚电路间的连接

2、TCP数据包中包括序号和确认

3、未按照顺序收到的包可以被排序,而损坏的包可以被重传

4、面向连接的服务(例如Telnet、FTP、rlogin、X Windows和SMTP)需要高度的可靠性,它们使用TCP来实现

 

UDP协议

1、UDP与TCP位于同一层

2、不对数据包的顺序进行检查

4、没有错误检测和重传机制

5、不被应用于那些使用虚电路的面向连接的服务

6、主要用于那些面向查询——应答的服务

例如NFS、NTP(网络时间协议)和DNS(域名解析协议)

    因此,欺骗UDP包比欺骗TCP包更容易,因为UDP没有建立初始化连接(也可以称为握手),即,与UDP相关的服务面临着更大的危险

数据通信的大体流程

数据通信的大体流程

地址和端口

地址:

1、物理地址

以太网内的物理地址是一个48bit的值,网卡生产过程中即固定

修改MAC地址的方法是

# ifconfig eth0 down

# ifconfig eth0 hw ether 00:01:02:03:04:05

# ifconfig eth0 up

2、ip地址

TCP/IP协议使计算机之间进行与底层物理网络无关的通信,可以跨越不同的局域网,甚至不同的物理网络

由32bit组成

{子网ID,主机ID}

子网ID表示由子网掩码覆盖的连续位

主机ID表示以外的位

A类地址:8bit子网ID,第一位为0

B类地址:16bit子网ID,前两位为10

C类地址:24bit子网ID,前三位为110

端口:

1、按照OSI七层协议的描述,传输层和网络层的最大区别是传输层提供进程通信能力

2、网络通信的最终地址不仅是主机地址,还包括可以描述进程的某种标识.

3、TCP/IP协议提出了端口的概念,用于标识通信的进程.

 

端口最大的意义在于它实现了进程与传输层之间的数据通道和标识,一个进程通过系统调用获取到一个端口后,传输层送到该端口的数据全部被该进程接收,同样,进程送交传输层的数据也通过该端口被送出。

端口号

类似于文件描述符,每个端口都拥有一个叫端口号的整数描述符,以区别不同的端口.

端口号的分配

TCP/IP端口号的分配综合了全局分配和本地分配两种方式,将端口号分成两部分

少量做为保留端口,以全局方式分配给服务进程,因此每个标准服务器都拥有一些全局公认的端口,即使在不同机器上,其端口号也相同。剩余的为自由端口,以本地方式分配.

TCP/UDP规定,小于256的端口才能作为保留端口.

0-1023范围内的端口是预留端口,这些端口只有超级用户才能使用,系统中常见的网络服务使用这些端口

1024-49151是已经注册的端口范围

49152-65535是可以自由使用的端口.

例如:常见端口号

ftp: 21

www: 80

Telnet: 23

 

网络的服务方式

网络层及其以下各层又称为通信子网,只提供了点到点的通信,没有程序或进程的概念;

传输层提供的是端到端的通信,引入网间进程的概念,同时也要解决差错控制,流量控制,数据排序,连接管理等问题。

进程间通信提供了不同的服务方式:

1、面向连接(虚电路)

2、无连接方式 

其中:

面向连接服务

A、电话系统服务模式的抽象

B、每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程

C、在数据传输过程中,各数据分组不携带目的地址,而使用连接号

D、本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同

E、TCP协议提供面向连接的虚电路

 

无连接服务

A、邮政系统服务的抽象

B、每个分组都携带完整的目的地址,各分组在系统中独立传送

C、不能保证分组的先后顺序

D、不进行分组出错的恢复和重传

E、不保证传输的可靠性

F、UDP协议提供无连接的数据报服务.

 

通信作用方式:

客户机/服务器模式

1、服务器方工作过程

1)打开一通信通道并告知本地主机,它愿意在一公认的地址端口(如80)上接收客户请求

2)等待客户请求到达该端口

3)接收客户请求并发送应答信号,激活一新进程处理这个客户请求

4)服务完成后,关闭新进程与客户的通信链路

5)主进程继续等待新的客户请求

6)关闭服务器.

2、客户机方工作过程

1)打开一通信通道并连接到服务器特定端口

2)向服务器发出服务请求,等待并接收应答

3)根据需要继续提出请求

4)请求结束后关闭通信通道并终止

 

 

 

下面正式进到socket 编程

概念

Socket(套接字)是BSD提供的网络应用编程界面,现在它已经是网络编程中的标准

Socket是一种特殊的进程间通信方式,不同机器上的进程都可以使用这种方式进行通信,网络中的数据传输是一种I/O操作,Socket也是一种文件描述符,它代表了一个通信管道的一个端点。read,write,close操作可应用于Socket描述符,在socket类型的文件描述符上,可以完成建立连接,数据传输等操作。

类型

常用的Socket类型有两种

流式Socket-SOCK_STREAM,提供面向连接的Socket

数据报式Socket-SOCK_DGRAM,提供面向无连接的Socket

 

socket 编程的大体过程

1、准备工作

主要解决ip地址的存放问题,这遇到一个字节序的问题

2、开始通信

1)创建套接字socket

2)分服务器和客户机端两种不同的情况分别处理

3、数据传输

4、关闭连接

具体如下图

socket编程的过程

 

 字节序的问题

由于历史的原因,业界存在两种字节序标准:BigEndian(大头、大端)和LittleEndian(小头、小端),Power PC是大头,X86是小头,有些CPU可以通过寄存器设置支持不同的字节序,例如MIPS;

所谓大头就是高位在低字节,低位在高字节;小头则与此相反,以0x345678为例,大头内存从低到高的存放次序为00,34,56,78,小头内存从低到高的存放次序为78,56,34,00;(上面的数值统一为16进制表示形式)

字节序问题广泛存在于设备与设备之间、单板与单板之间、单板与底层芯片之间,只要两个处理单元的字节序不同,这个问题就存在,为了解决不同字节序的处理单元之间的通信问题,业界定义了主机序和网络序的概念,网络序主要用于信息传递,一般不用于计算,其字节顺序与大头一致;

除了在编码时绷紧这根弦以外,我们在器件选择时尽量选择主机序与网络序一致的芯片,同一设备的不同单板使用相同的字节序,并优先选择支持大头的芯片,这样,即使不能彻底解决问题,也可以彻底规避问题。

解决办法:

字节序转换函数

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostint32);

uint16_t htons(uint16_t hostint16);

以上返回网络字节序

 

uint32_t ntohl(uint32_t netint32);

uint16_t ntohs(uint16_t netint16);

以上返回主机字节序

网络地址的存储形式

因特网地址定义在<netinet/in.h>中.在IPV4因特网域(AF_INET)中,套接字地址用如下结构sockaddr_in表示.

struct in_addr

{

in_addr_t   s_addr;

};

struct sockaddr_in

{

sa_family_t     sin_family; //协议族

in_port_t        sin_port; //端口号

struct in_addr  sin_addr; //ip地址

unsigned char   sin_zero[8]; //初始化为全 0 

}; 

 

强制转换地址成通用的地址结构sockaddr

struct sockaddr

{

    sa_family_t sa_family;

    char    sa_data[14];

   };

 

struct in_addr以一个32位无符号数来表示,通常需要用到点分十进制数串与它之间的转换:

int inet_pton(int family, const char *strptr, void *addrptr);

成功返回1,否则返回0

const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

len代表strptr缓冲区的长度

#define INET_ADDRSTRLEN 16

 

创建套接字

创建套接字是进行任何网络通信时必须做的第一步

创建一个用于网络通信的I/O描述符(套接字)

相当于在对文件读写前先用open获取文件描述符

#include <sys/socket.h>

int socket(int family, int type,int protocol);

family:协议族

AF_INET,AF_INET6,AF_LOCAL,AF_ROUTE,AF_KEY

type:套接口类型

SOCK_STREAM,SOCK_DGRAM,SOCK_SEQPACKET,SOCK_RAW

protocol:协议类别

0,IPPROTO_TCP,IPPROTO_UDP,IPPROTO_SCTP

返回值:套接字

 

创建服务器端

做为服务器需要具备的条件:

1)、具备一个可以确知的地址,以便让别人找到我

2)、使用socket创建一个套接字时,系统不会分配一个理想的端口

3)、让操作系统知道你是一个服务器,而不是一个客户端

4)、使用socket创建的是主动套接字,但是作为服务器,需要被动等待别人的连接

5)、等待连接的到来,对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始

 

1、使用一个确知的端口来接收客户端的连接

bind函数将一个地址绑定到套接字

#include <sys/socket.h>

int bind(int sockfd,

const struct sockaddr *myaddr,

socklen_t addrlen);

sockfd:socket套接口描述字

myaddr:指向特定于协议的地址结构指针

addrlen:该地址结构的长度

返回值:0,成功;其他,失败

 

2、让套接字成为被动的

Listen函数可以将套接口由主动修改为被动,操作系统为该套接口设置一个连接队列,来记录所有连接到该套接口的连接

#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd:socket套接口描述字

backlog:连接队列的长度

返回值:0,成功;其他,失败

 

3、等待连接的到来

Accept函数从连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待

#include <sys/socket.h>

int accept(int sockfd,

struct sockaddr *cliaddr, 

      socklen_t *addrlen);

sockfd:socket套接口描述字

cliaddr: 客户端地址

addrlen:客户端地址结构体长度

返回值:已连接的套接口

注:accept函数返回的是一个套接口,这个套接口代表了当前这个连接

 

创建客户端

作为客户端需要具备的条件:知道服务器的IP地址以及端口号。相对服务器端,它很简单,只需建立连接。

客户端需要主动跟服务器建立连接,连接建立才可以开始传输数据(对于TCP协议)

#include <sys/socket.h>

int connect(int sockfd,

const struct sockaddr *addr,

socklen_t len);

sockfd:socket套接口描述字

addr: 服务器地址

addrlen:服务器地址结构体长度

返回值:0,成功;其他,错误

connect函数建立连接之后不会产生新的套接口

 

数据通信

当连接建立后,通信的两端便具备了两个套接口

套接口也是一种文件描述符,所以read、write函数可以用于从这个通信管道取出或向其写入数据

发数据:

#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, 

           size_t nbytes, int flags);

ssize_t sendto(int sockfd, const void 

           *buf, size_t nbytes, int flags, 

           const struct sockaddr *destaddr, 

           socklen_t destlen);

  ssize_t sendmsg(int sockfd, const struct 

           msghdr *msg, int flags);

相应的收数据:

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, 

           size_t nbytes, int flags);

ssize_t recvfrom(int sockfd, void 

     *restrict buf, size_t len, int flags, 

      struct sockaddr *restrict addr, 

      socklen_t *restrict addrlen);

  ssize_t recvmsg(int sockfd, 

           struct msghdr *msg, int flags);

 

关闭连接

使用close函数即可关闭套接字

关闭一个代表已建立连接的套接字将导致另一端接收到一个0长度的数据包

做服务器时,关闭socket创建的套接字将导致服务器无法继续接受新的连接,但不会影响已经建立的连接;关闭accept返回的套接字将导致它所代表的连接被关闭,但不会影响服务器的监听

做客户端时,关闭连接就是关闭连接,不意味着其他

 

例子:(只建立了服务器端,由windows 的telnet 测试 )

#include <stdio.h>

#include <stdlib.h>

#include <string.h> // bzero

#include <unistd.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h> // inet_ntop

 

int main(int argc, char *argv[])

{

char recvbuf[2048]; // 接收缓冲区

int sockfd; // 套接字

struct sockaddr_in servAddr; // 服务器地址结构体

unsigned short port = 8000; // 监听端口

 

if(argc > 1) // 由参数接收端口

{

port = atoi(argv[1]);

}

printf("TCP Server Started at port %d!/n", port);

sockfd = socket(AF_INET, SOCK_STREAM, 0);

// 创建TCP套接字

if(sockfd < 0)

{

perror("Invalid socket");

exit(1);

}

bzero(&servAddr, sizeof(servAddr)); // 初始化服务器地址

servAddr.sin_family = AF_INET;

servAddr.sin_port = htons(port);

servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

printf("Binding server to port %d/n", port);

if(bind(sockfd, (struct sockaddr*)&servAddr, sizeof(struct sockaddr)) != 0)

{

close(sockfd);

perror("binding err!");

exit(1);

}

if(listen(sockfd, 1) != 0)

{

close(sockfd);

perror("listen err!");

exit(1);

}

printf("waiting client.../n");

while(1)

{

char cliIP[INET_ADDRSTRLEN]; // 用于保存客户端IP地址

 

size_t recvLen;

struct sockaddr_in cliAddr; // 用于保存客户端地址

size_t cliAddrLen = sizeof(cliAddr);

// 必须初始化!!!

int connfd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliAddrLen);

// 获得一个已经建立的连接

if(connfd < 0)

{

close(sockfd);

perror("accept err!");

exit(1);

}

inet_ntop(AF_INET, &cliAddr.sin_addr.s_addr, cliIP, INET_ADDRSTRLEN);

printf("client ip = %s/n", cliIP);

while((recvLen = read(connfd, recvbuf, 2048)) > 0)

{

write(connfd, recvbuf, recvLen);

}

close(connfd);

printf("client closed!/n");

}

close(sockfd);

return 0;

}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值