Linux网络编程入门

一  总述

1.1 七层/五层模型

1.2  Linux下协议栈框架

1.3  IP 子网掩码概念

IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。子网掩码(subnet mask)又叫网络掩码地址掩码、子网络遮罩,它是一种用来指明一个IP地址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成网络地址主机地址两部分。

 

 

二  TCP/IP编程基础知识

 

2.1 Socket编程

2.1.1 Socket介绍
• 网络通信中应用进程和网络协议之间的接口,为TCP/IP协议开发的应用程序接口
• 以socket文件描述符作为参数,系统调用从用户空间切换到内核空间,从而进入到BSDSocket层的操作。
• 操作的对象是socket{}结构,每一个这样的结构对应的是一个网络连接。

2.1.2 网络连接函数
• socket
• bind
• connect
• listen
• accept
• select
• recv, recvfrom
• send, sendto
• close, shutdown

2.1.3    数据结构:sockaddr
struct sockaddr {
unsigned short sa_family; /* address family,
AF_xxx */
char sa_data[ 14]; /* 14 bytes of protocol address */
};
此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息

数据结构:sockaddr_in
struct sockaddr_in {

short int sin_family;/* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr;/* Internet address */
unsigned char sin_zero[ 8]; /* Same size a struct sockaddr */

};
该结构与sockaddr兼容,供用户填入参数      程序中实际只填写sockaddr_in结构
 

2.1.4  函数简介:

(1)socket
• Socket描述符与Linux中的文件描述符类似,也
是一个int型的变量
• 函数调用:
– int socket(int domain, int type, int protocol);
– 函数返回Socket描述符,返回-1表示出错
– domain参数只能取AF_INET, protocol参数一般取0
• 应用示例:
– TCP:sockfd = socket(AF_INET,SOCK_STREAM,0);
– UDP:sockfd =socket(AF_INET, SOCK_DGRAM,0);
(2) bind
• 作为Server程序,需要与一个端口绑定
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
bind函数返回-1表示出错,最常见的错误是该端口已经被其他程序绑定。需要注意的一点:在Linux系统中,1024以下的端口只有拥有root权限的程序才能绑定.

 (3) connect
连接某个Server
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
• servaddr是事先填写好的结构,Server的IP和端口都在该数据结构中指定

 

(4) listen
• 开始监听已经绑定的端口
– 需要在此前调用bind()函数,否则由系统指定一个随机的端口
– int listen(int sockfd, int queue_length);
• 接收队列– 一个新的Client的连接请求先被放在接收队列中,等待Server程序调用accept函数接受连接请求
– queue_length指的就是接收队列的长度也就是在Server程序调用accept函数之前最大允许的连接请求数,多余的连接请求将被拒绝

(5) accept()函数将响应连接请求,建立连接
– 产生一个新的socket描述符来描述该连接
– 这个连接用来与特定的Client交换信息
• int accept(int sockfd,struct sockaddr *addr,int *addrlen);
– addr将在函数调用后被填入连接对方的地址信息,如对方的IP、端口等。
• accept缺省是阻塞函数,阻塞直到有连接请求

(6) recv
用于TCP协议中接收信息
int recv(int sockfd, void *buf, int len, int flags);
• buf,指向容纳接收信息的缓冲区的指针
• len,缓冲区的大小               • flags,接收标志
• 函数返回实际接收的字节数,返回-1表示出错
• recv缺省是阻塞函数,直到接收到信息或出错

(7):recvfrom
用于UDP协议中接收信息
int recvfrom(int sockfd,void *buf,int len,unsigned int flags
struct sockaddr *from, int *fromlen);
• buf,指向容纳接收信息的缓冲区的指针
• len,缓冲区的大小
• flags,接收标志
• from,指明接收数据的来源
• 函数返回实际接收的字节数,返回-1表示出错
• recvfrom是阻塞函数,直到接收到信息或出错

(8):send
用于TCP协议中发送信息
int send(int sockfd, const void *msg, int len, int flags);
– msg,指向待发送信息的指针
– len,待发送的字节数
– flags,发送标志
• 函数返回已发送的字节数,返回-1表示出错
• send缺省是阻塞函数,直到发送完毕或出错
• 注意:如果函数返回值与参数len不相等,则剩余的未发送信息需要再次发送

(9):sendto
用于UDP协议中发送信息
int sendto(int sockfd, const void *msg, int len, unsigned int
flags, const struct sockaddr *to, int tolen);
– buf,指向容纳接收信息的缓冲区的指针
– len,缓冲区的大小
– flags,接收标志
– to,指明发送数据的目的地
• 函数返回已发送的字节数,返回-1表示出错
• sendto缺省是阻塞函数,直到发送完毕或出错
• 注意:如果函数返回值与参数len不相等,则剩余的未发送信息需要再次发送

(10) close
• 关闭特定的socket连接
• 调用函数:int close(int sockfd);
• 关闭连接将中断对该socket的读写操作。
• 关闭用于listen()函数的socket将禁止其他Client的连接请求

三  TCP应用举例

3.1   Server端

3.1.1 Server程序的作用
• 程序初始化
• 持续监听一个固定的端口
• 收到Client的连接后建立一个socket连接
• 与Client进行通信和信息处理
– 接收Client通过socket连接发送来的数据,进行相应处理并返回处理结果,如BBS Server
– 通过socket连接向Client发送信息,如Time Server
• 通信结束后中断与Client的连接

3.2   程序流程

程序流程一
• 取得socket描述符:
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM,0)) ;   //TCP

程序流程二
• 填写自身地址信息的sockaddr_in结构
struct sockaddr_in my_addr; /* 自身的地址信息 */
my_addr.sin_family = AF_INET;
/* 网络字节顺序 */
my_addr.sin_port = htons(MYPORT);
/* 自动填本机IP */
my_addr.sin_addr.s_addr = INADDR_ANY;
/* 其余部分置0 */
bzero(&(my_addr.sin_zero), 8);

程序流程三
• 绑定端口
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));


程序流程四
• 监听端口
#define BACKLOG 10
listen(sockfd, BACKLOG);

程序流程五
• 接受连接请求

int new_fd;/* 数据端口 */
struct sockaddr_in their_addr; /* 连接对方的地址信息 */
int sin_size;
sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, (struct sockaddr *)&their_addr,&sin_size));


程序流程六
• 产生新进程(线程)处理读写socket

if (!fork()) {/* 子进程 */
if (send(new_fd, "Hello, world!\ n", 14, 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd);


程序流程七
• 转程序流程五,继续等待其他Client的连接并处理
 

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MYPORT 3490 /* 监听端口 */
#define BACKLOG 10 /* listen的请求接收队列长度 */
void main()
{
int sockfd, new_fd; /* 监听端口,数据端口 */
struct sockaddr_in my_addr; /* 自身的地址信息 */
struct sockaddr_in their_addr; /* 连接对方的地址信息 */
int sin_size;

//socket一波
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT); /* 网络字节顺序 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 自动填本机IP */
bzero(&(my_addr.sin_zero), 8); /* 其余部分置0 */

//bind一波
if (bind(sockfd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr)) == -1) {
perror("bind");
exit(1);
}

//listen一波
if (listen(sockfd, BACKLOG) == -1) {
perror("listen");
exit(1);
}
while(1) { /* 主循环 */
sin_size = sizeof(struct sockaddr_in);

//accept一波
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size))
if (new_fd == -1) {
perror("accept");
continue;
}
printf(”Got connection from %s\ n", inet_ntoa(their_addr.sin_addr));
if (!fork()) { /* 子进程 */

//send一波
if (send(new_fd, "Hello, world!\ n", 14, 0) == -1) perror("send");
close(new_fd);
exit(0);
}

//close一波
close(new_fd); /* 无法生成子进程时有用 */
while(waitpid(-1,NULL,WNOHANG) > 0); /*清除所有子进程 */
}
}

注意Tcp Server 不断循环调用accept函数检测是否有新的连接,accept缺省是阻塞函数,阻塞直到有连接请求,有的话accept()函数将响应连接请求,建立连接,然后我们就可以新开一个进程(或者线程)来对这个已经建立起来的TCP连接进行读写操作。

•int new_fd= int accept(int sockfd,struct sockaddr *addr,int *addrlen)

--@return 一个新的socket描述符来描述该连接 (即new_fd),对于该已经建立起来的连接此后均使用new_fd(有状态的)-– --@argument addr将在函数调用后被填入连接对方的地址信息,如对方的IP、端口等。

 


– 产生
– 这个连接用来与特定的Client交换信息

3.2 Tcp  Client例子

直接上代码

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 3490 /* Server的端口 */
#define MAXDATASIZE 100 /*一次可以读的最大字节数 */
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he; /* 主机信息 */
struct sockaddr_in their_addr; /* 对方地址信息 */
if (argc != 2) {
fprintf(stderr,"usage: client hostname\n");
exit(1);
}
if ((he=gethostbyname(argv[1])) == NULL) {
/* get the host info */
herror("gethostbyname");
exit(1);
}

//socket一波
if ((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) {
perror("socket");
exit(1);
}
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(PORT); /* short, NBO */
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero), 8); /* 其余部分设成0 */91

//connnect一波   connect是客户端用于连接server的
if (connect(sockfd, (struct sockaddr *)&their_addr,
sizeof(struct sockaddr)) == -1) {
perror("connect");
exit(1);
}

//receive一波
if ((numbytes=recv(sockfd,buf,MAXDATASIZE,0))==-1) {
perror("recv");
exit(1);
}
buf[numbytes] = '\0';
printf("Received: %s",buf);

//close一波
close(sockfd);
return 0;
}

对于TCP client,调用connect来连接某个Server
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
• servaddr是事先填写好的结构,Server的IP和端口都在该数据结构中指定

connect函数调用后将会修改参数sockfd,使之成为有状态的

 

3.3  总结 

TCP Client流程    socket------->connect-------->(send)------>recv------->close

TCP Server流程    socket------->bind-------->listen------->accept------>(recv)------->send-------->close

 

 

四  UDP编程

UDP编程与TCP类似,区别在于:

  • TCP 是面向连接的,UDP 是面向无连接的(所以有了recv和recvfrom两种,同样所以有了send和sendto两种函数)
  • UDP程序结构较简单
  • TCP 是面向字节流的,UDP 是基于数据报的
  • TCP 保证数据正确性,UDP 可能丢包
  • TCP 保证数据顺序,UDP 不保证

所以,UDP编程的适用范围
• 部分满足以下几点要求时,应该用UDP
– 面向数据报
– 网络数据大多为短消息
– 拥有大量Client
– 对数据安全性无特殊要求
– 网络负担非常重,但对响应速度要求高
• 例子:ICQ、ping

 

五    Linux网络程序的设计

 


5.1  总述    主要针对TCP Server网络编程
• 协议设计
• Server程序的设计
– 程序结构的考虑
– 传输模式的考虑
– 网络函数的考虑

5.2    设计网络协议
• 设计网络协议目的
• 设计网络协议时需要考虑的因素
完备性
正确性
最简性(简便性)

示例:Chat协议
• 协议分成两组:
– 客户端请求协议
– 服务器通知协议
• 发送协议后对方会返回一个数字表示错误值,可以根据错误值判断是否完成操作和/或错误类型
110
客户端请求协议

普通用户命令:
用户申请登录 0x80 用户退出 0x81
用户加入房间 0x82 用户离开房间 0x83
给用户发消息 0x84 在房间中讲话 0x85
取所有房间的信息 0x86 取某一房间的信息 0x87
取所有用户的信息 0x88 取某一用户的信息 0x89
管理员命令:
对所有用户广播 0x90
踢出用户 0x91
增加房间管理员 0x92


服务器通知协议

用户加入组0xC0
用户离开组0xC1
传递消息0xC2
用户成为房间管理员0xC3
用户被踢出房间0xC4


112
对协议代码的返回值
• 0x7F:无错误(正确返回)。
• 0xF0:无效的房间名称。
• 0xF1:无效的用户名称。
• 0xF2:用户登录失败。
• 0xF3:当前用户没有此项操作的权力。
• 0xF4:达到最大房间数,无法新建房间。
• 0xF5:达到最大用户数,无法登录。
• 0xFE:无效协议代码
• 0xFF:其他错误

5.3      Server的设计
• 设计Server时需要考虑的因素
响应速度(新建连接时、发送数据时)
运行速度
I/O吞吐量
其它:流量控制(QoS)、安全性
针对特定协议的数据结构

5.3.1    设计Server的程序结构
• 程序结构的考虑
– 多线程
– 多进程
– 单进程
• 网络函数的考虑
– TCP流模式 或 UDP数据报模式
– 阻塞函数 或 非阻塞函数


程序结构:多进程
• 在主进程调用accept()函数生成一个新的连接后,调用fork()产生一个子进程对这个新连接进行操作(如果子进程和父进程对变量只读,也就是说变量不会被改变,这时候,变量表现为共享的,此时物理空间只有一份。如果说父进程者子进程需要改变变量,那么进程将会对物理内存进行复制,这个时候变量是独立的,也就是说,物理内存中存在两份空间,又称写时复制而通过bind之后返回的数据端口new_fd应该是只读的,父子进程共享这一变量

• 在主进程结束前需要向所有子进程发中断信号并等待所有子进程执行完毕。
• 这种程序结构最简单,例子可以参照前面TCP Server的结构和代码。
• 主要应用于各连接操作相互独立的Server,可
以保证各连接相互间的数据安全性, 如telnetd

程序结构:多线程
• 基本与多进程结构类似,但是在获得新连接时生成一个线程来对这个连接进行处理。
• 主要的优点:
– 线程调度速度快,占用资源少
– 线程可共享进程空间中的数据
• 主要应用于各个连接之间关系较紧密的Server,
例如:BBS Server
• Server的响应速度和I/O吞吐量均较好,是最常用的程序结构


程序结构:单线程
• 通过select实现非阻塞的同步I/O模式
• 可以通过调用select函数得出需要读数据并处理的socket集合(也就是Client的集合),然后依次对每个socket读数据,处理并向socket写结果
• select得到的socket列表中有一个特殊的socket就是listen函数使用的socket,这个socket需要单独
处理,调用accept生成新的socket连接并将这个新socket加入已有的socket集合。
• 该结构对算法效率要求较高,一般来说响应速度慢,但I/O处理速度最快。适用于连接数少、大数据吞吐量的Server 

以下是一个单线程的server

int listenfd, connfd, maxfd=0;
int nready;
fd_set rset, allset;
struct sockaddr_in cliaddr, servaddr;
int clilen;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd > maxfd) maxfd = listenfd;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(4321);
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
listen(listenfd, 10); 119
while (1) {
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr,&clilen);
if (client_num == FD_SETSIZE) {
fprintf(stderr, "too many clients\n");
exit(-1);
}
FD_SET(connfd, &allset);
if (connfd > maxfd) maxfd = connfd;
if (--nready <= 0) continue;
}
//以下依次判断FD_ISSET(某个socket, &rset) 并做相应处理
} 


5.3.2   特殊的要求
• 用户线程池
– 应用于多线程程序结构,提供用户线程的支持,减少管态/目态切换和线程调度时间,提高切换效率,并增大系统支持的线程数。
• 内存池
– 一般用于Client连接个数极多且经常性变化的Server,它对内存的分配和管理要求较高,因此用独立的内存池来实现用户级的内存分配和管理,提高程序效率,且有利于程序调试。
• 缓冲池
– 对网络数据先大批存入缓冲池再进行响应操作,有利于减少对设备I/O的访问次数,提高程序运行效率,同时对I/O吞吐量大的Server避免了丢包的问题

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值