null实验六 TCP、UDP通信程序
实验实验六 TCP、UDP通信程序设计实验null*客户1客户2服务器请求应答请求应答通信程序基础:客户端和服务器null*IPv4, IPv6Ethernet AdapterApp1App2port Aport BTCPUDP通信程序基础:分层TCP通信实验cont1*TCP通信实验cont1null*http://infonet.ustc.edu.cn/image/headline.gif
得到ip地址
建立tcp连接:
构造http数据包,向tcp请求
构造tcp数据包,承载http包
构造ip数据包
得到mac地址
构造数据帧通信程序基础:实例null*
Socket介绍(1)*
Socket介绍(1)什么是socket?
先看文件操作的例子:
通过open函数得到一个文件的文件描述符;然后对这个描述符进行读写得到一个整数来标识这个文件,称为file descriptor得到一个整数来标识这个文件把用来标识这个文件整数看作是这个入口的标识把用来标识这个文件的整数看作是这两入口的标识Socket介绍(2)*Socket介绍(2)socketsocketSocket descriptor用类似于open的函数得到一个socket 描述符,然后对这个描述符进行读写操作Socket介绍(3)*从网络整体来看,socket是不同主机上应用程序之间的一个虚拟的接口,具有跨平台特性。
从程序员角度来看,它是应用程序和网络设备的一个接口,特殊的I/O
从操作系统看,它是一种资源。如同handle用来描述windows中的窗口等资源,socket用socket descriptor来标识。Socket介绍(3)Socket介绍(4)*字节流套接口(Stream Sockets)
面向连接的,位于TCP之上
数据报套接口(Datagram Sockets)
无连接的,位于UDP之上
原始套接口(Raw Sockets)
直接发送和处理IP包:例如pingSocket介绍(4)Socket介绍(5)*Socket的起源
最初在70年代由加州大学Berkeley分校开发,其目的是为BSD(Berkeley Software Distribution) UNIX 4.1版操作系统提供网络通信接口。
Socket在各种平台下的发展
随着Berkeley Sockets的广泛应用,九十年代初,Sun、MS等公司共同制定了适应dos和win平台的windows sockets的规范(WinSock)
Sun Microsystems为Java也制定了网络通信的API
Linux下的socket继承了BSD sockets的风格,并有所改动
不同的网络有不同的套接口
CCITT X.25套接口
Berkeley 套接口
Socket介绍(5)Socket介绍(6)*现在开始具体介绍socket编程。
由前面的叙述可以看出,我们要进行通信就要得到对应的socket descriptor,一旦得到了,对它进行读写操作就可以了,例如
int read( int fd, char *buf, int len);
int write(int fd, char *buf, int len);
在与另一台计算机通信之前要知道对方的什么信息?
IP地址或者域名
端口(用来区分不同的应用)Socket介绍(6)Socket介绍(7)*Socket用下面的结构体来描述一个IP地址socket.h
结构中sa_family为套接口的
族地址类型,例如对于通常的TCP/IP协议(IPv4),它的值是AF_INET; sa_data中存储着具体的协议地址,不同的协议族有不同的地址格式。
sa_data的存储
往往是包含地址和端口信息,而仅使用一个变量,使用起来不太方便——定义新的Socket地址结构Socket介绍(7)struct sockaddr
{
unsigned short sa_family; /* 地址家族2字节*/ char sa_data[14]; /*14字节协议地址*/
}; Socket介绍(8)*新版的socket地址的定义
最后的那个元素是填充的空白信息, 这样就保持整个结构与sockaddr结构的长度相同Socket介绍(8)struct sockaddr_in
{
short int sin_family; /* 通信类型2字节 */
unsigned short int sin_port; /* 端口, 2字节*/
struct in_addr sin_addr; /* Internet 地址, 4字节*/
unsigned char sin_zero[8];
}注意理解指针和类型强制转换Socket介绍(9)*Socket介绍(9)struct in_addr
{
unsigned long s_addr;
};图中的紫色部分就是端口和IP地址。这两个域必须是网络字节顺序
Network Byte Orderstruct sockaddr_in
{
short int sin_family; /* 通信类型2字节 */
unsigned short int sin_port; /* 端口, 2字节*/
struct in_addr sin_addr; /* Internet 地址, 4字节*/
unsigned char sin_zero[8];
}null*struct in_addr{
union{
struct{
unsigned char s_b1;
unsigned char s_b2;
unsigned char s_b3;
unsigned char s_b4;
} S_un_b;
struct{
unsigned short s_w1;
unsigned short s_w2;
} S_un_w;
unsigned long S_addr;
} S_un;
}; Socket介绍(10)*sockaddr只是一个抽象的概况形式,并不实用
sockaddr_in更加结合了TCP/IP协议族的特点,易于使用,所以编程中常使用这个结构。
但是:socket本身所封装的API都是支持sockaddr结构的,所以填充sockaddr_in结构需要强制转换成sockaddr结构,方可作为参数被socket的标准函数所使用。例如:
int connect( int sockfd, struct sockaddr * servaddr, unsigned int addrlen)Socket介绍(10)字节顺序*字节顺序
主机字节顺序(Host Byte Order)
低位在前,高位在后(little-endian)
基于Intel芯片的机器采取这种存储方式
网络字节顺序(Network Byte Order)
高位在前,低位在后(big-endian)
Sockaddr_in的变量成员(端口和地址)都必须使用网络字节顺序字节顺序null*例如端口34567的16进制表示是0x8707。如果定义变量unsigned short sin_port = 345670x111111110x11111112主机字节序….68 1f e3 34 87 07 36….网络字节序以字节为最小单位,但是计算机对内存的读取是双字节的如果是ip地址202.38.75.11的16进制表示。如果定义变量unsigned long s_addr = 0xCA264B0B0x222222220x222222250B4B26CA….68 1f CA 26 4B 0B 12….字节顺序*字节顺序转换字节顺序的函数(in.h)uint16_t htons(uint16_t);
uint16_t ntohs(uint_16_t);
uint32_t htonl(uint32_t);
uint32_t ntohl(uint32_t);‘h’ : host
‘n’ : network
‘s’ : short (16bit) ‘l’ : long (32bit)
例如:定义struct sockaddr_in sh; unsigned short port = 12345; sh.sin_port = htons(port);htons和ntohs的实现是一样的;htonl和ntohl一样字节顺序*前面提到的几个函数对于IP地址的转换仍然不方便,因为首先得得到ip地址的数值表示,而我们习惯于用带点的字符串来表示,如“202.38.75.11”
因此希望有函数能处理这样的请求:
给一个字符串如“202.38.75.11”能返回相应的网络字节序的unsigned long值
给一个unsigned long的值能返回一个字符串字节顺序null*inet_aton
int inet_aton(const char *cp, struct in_addr *inp);
例如将“192.168.0.10”转化为0xC0A8000A
inet_addr
unsigned long inet_addr(const char *cp);
功能同上,但不能处理广播地址
inet_ntoa
char * inet_ntoa(struct in_addr in);
例如将0xC0A8000A转化为“192.168.3.10”TCP通信实验*TCP通信实验注意bind和listensocket()bind()listen()accept()write()read()read()TCP Serverclose()socket()TCP Clientconnect()write()read()close()建立连接客户请求服务器响应结束连接基本套接口函数(1)- socket()*基本套接口函数(1)- socket()int fd; /* socket descriptor */
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) }
fprintf(stderr,“socket creating error\n”);
exit(1);
}/*注:在TCP或UDP编程的时候,protocol都取0*/#include
int socket(int domain, int type, int protocol);
创建socket 返回:非负整数描述符表示成功,-1表示出错
domain一般设为AF_INET,protocol一般设为0null*基本套接口函数(2)- connect()#include
int connect(int sockfd, struct sockaddr * servaddr,
unsigned int addrlen);
返回:0表示成功,-1表示出错 connect()由客户使用, 旨在和服务器建立一个连接。sockfd是函数socket()返回的套接口描述符, servaddr表示远程服务器的套接口, addrlen表示套接口地址的长度
注意:之前要先调用socket()创建套接口null*TCP通信- 客户端例子int fd; /* 套接口描述符 */
struct sockaddr_in srv; /* 套接口地址结构 */
fd = socket(AF_INET, SOCK_STREAM, 0);
/* connect: AF_INET表示使用Internet地址族 */
srv.sin_family = AF_INET;
/* connect: 目标是连向服务器的 8000 号端口 */
srv.sin_port = htons(8000);
/* connect: 目标服务器的 IP Address 是 “202.38.75.11” */
srv.sin_addr.s_addr = inet_addr(“202.38.75.11”);
if(connect(fd, (struct sockaddr*) &srv, sizeof(srv)) < 0) {
fprintf(stderr, ”connect error!\n");
exit(1);
}null*基本套接口函数(3)- bind()#include
int bind(int sockfd, struct sockaddr * servaddr,
unsigned int addrlen);
返回:0表示成功,-1表示出错 bind将本机地址(某个或全部地址)与套接口绑定在一起
一般用于服务器绑定自己公认的服务端口号
客户端一般会在调用connect函数时,系统自动为客户
选择一个大于1024的端口号,并用客户本地IP地址填充
套接口地址中的相关项null*基本套接口函数(4)- listen()#include
int listen(int sockfd, int backlog);
返回:0表示成功,-1表示出错 listen只被TCP服务器所使用!
函数listen将一个套接口转换为侦听套接口(listening socket), 因为每个套接口在创建的时候都是主动套接口,等待使用connect函数发起连接。而listen将套接口转化为被动的,指示内核应接收来自此套接口的连接请求。
backlog参数指示了内核为此套接口排队的最大连接数目基本套接口函数(5)- accept()*基本套接口函数(5)- accept()#include
int accept(int sockfd, struct sockaddr * addr,
unsigned int * addrlen);
返回:非负描述符表示成功,-1表示出错函数accept由TCP服务器在listen函数之后调用, 它从侦听的套接口的完成连接队列中接收一个连接, 若已完成连接为空, 那么该进程进入睡眠, 处于等待连接的方式
参数sockfd指定侦听的套接口描述符, addr和addrlen用以返回与服务器相连接的客户的协议地址信息, 如果对客户地址和端口感兴趣, 则可以从addr中提取相关信息
函数accpet最终返回一个新的套接口描述符, 以标识连接该函数是阻塞型的!!!基本套接口函数(6)- 其它*基本套接口函数(6)- 其它#include
int read( int fd, char *buf, int len);
返回实际接收的缓冲区大小
int write(int fd, char *buf, int len);
返回实际发送的缓冲区大小
int close(int sockfd);
int closesocket(int sockfd);
成功返回0, 否则返回-1
还有一些其他的发送接收函数, 感兴趣者可以查阅帮助null*TCP通信- 服务器例子int fd; /* 套接口描述符 */
struct sockaddr_in srv; /* 套接口地址结构 */
fd = socket(AF_INET, SOCK_STREAM, 0)
/* AF_INET表示使用Internet地址族 */
srv.sin_family = AF_INET;
/* 将socket绑定到 8000 号端口,将主机存储方式转化为网络存储方式 */
srv.sin_port = htons(8000);
/* bind: INADDR_ANY 表示服务器将接收来自本机上任何一块网卡的客户连接
将主机存储方式转化为网络存储方式 */
srv.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(fd, (struct sockaddr*) &srv, sizeof(srv)) < 0) {
fprintf(stderr, ”bind error!\n");
exit(1);
}null*if(listen(fd, 5) < 0) {
fprintf(stderr, ”listen error!\n");
exit(1);
}
newfd = accept(fd, (struct sockaddr*) &cli, &cli_len);
if(newfd < 0) {
fprintf(stderr, "accept error\n");
exit(1);
}TCP通信- 服务器例子(续)实验二、TCP、UDP通信程序设计实验
——TCP通信实验注意事项*要求使用linux编写通信程序
Linux下,写好源代码后,gcc file.c –o xxx;运行则用./xxx
每调用一个函数要进行错误检查及处理
例如 if(listen(fd, 5) < 0) …
socket用完之后要调用close关闭连接
进一步的思考
程序的水平取决于连接建立后的读写操作的设计,当然良好的用户界面也很重要
accept函数是一个阻塞型函数——多线程的用武之地实验二、TCP、UDP通信程序设计实验
——TCP通信实验注意事项null*UDP通信实验*UDP通信实验socket()bind()recvfrom()sendto()UDP Serversocket()UDP Clientsendto()recvfrom()close()阻塞,直到接收到
客户发送过来的数据报data requestdata replynull*基本套接口函数(7)#include
int recvfrom(int sockfd, void * buf, int len,
int flags, struct sockaddr * from,
unsigned int *addrlen);
int sendto(int sockfd, const void * msg, int len,
int flags, const struct sockaddr * to,
unsigned int addrlen);与TCP不同的是, UDP在通信时, 系统内部不记录套接口地址信息, 都是函数中主动以参数的形式指明的. 在TCP情况下, 客户connect成功后(服务器accept成功后), 每次发送接收都只需指定套接口描述符就行了, 但是这里每次发送接收都需要额外附加上对方的套接口地址信息.阻塞型函数服务器地址信息,由程序员填写返回发送方地址信息,由函数填写UDP通信——服务器端例子*UDP通信——服务器端例子int fd; /* 套接口描述符 */
struct sockaddr_in srv; /*服务器绑定的套接口地址信息*/
fd = socket(AF_INET, SOCK_DGRAM, 0);
srv.sin_family = AF_INET;
srv.sin_port = htons(8000);
srv.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(fd, (struct sockaddr*) &srv, sizeof(srv))<0) {
fprintf(stderr, "bind error!\n"); exit(1);
}null*struct sockaddr_in cli;
char buf[512];
int cli_len = sizeof(cli);
int nbytes;
nbytes = recvfrom(fd, buf, sizeof(buf),0/*flags*/,
(struct sockaddr*) &cli, &cli_len);
if(nbytes < 0) {
fprintf(stderr,“recvfrom error\n”); exit(1);
}
UDP通信——服务器端例子(续)UDP通信——客户端例子*UDP通信——客户端例子int fd; /* socket descriptor */
struct sockaddr_in srv; /* used by sendto() */
/* 1) create the socket */
/* sendto: send data to IP Address “128.2.35.50” port 80 */
srv.sin_family = AF_INET;
srv.sin_port = htons(8000);
srv.sin_addr.s_addr = inet_addr(“192.168.6.51”);
nbytes = sendto(fd, buf, sizeof(buf), 0 /* flags */,
(struct sockaddr*) &srv, sizeof(srv));
if(nbytes < 0) {
fprintf(stderr,“sendto error\n”); exit(1);
}