socket编程的基本知识

我的看书笔记,好像是《Unix网络编程》这本书。记录如下:

————
每一个Socket都用一个半相关描述:
{协议,本地地址,本地端口}
一个完整的Socket则用一个相关描述
{协议,本地地址,本地端口,远程地址,远程端口}
每一个Socket有一个本地的唯一Socket号,由操作系统分配。最重要的是,Socket是面向客户-服务器模型而设计的,针对客户和服务器程序提供
不同的Socket系统调用。

套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字。
流式套接字是通过使用TCP协议来保证应用层次上的数据传输质量的。
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,它使用UDP报文。使用UDP的程序都要有自己的对数据进行确认的协议,如tftp协议,我们在用tftp传数据的时候,tftp的四层协议使用的UDP协议,在传数据的时候每传一个包就会检查是否回了ack包,这个检查在tftp报头上完成。
原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。

每一个机器内部对变量的字节存储顺序不同,而网络传输的数据大家是一定要统一顺序的,因此要定义网络字节序。
htons(), htonl(), ntohs(), ntohl().
在struct sockaddr_in中的sin_addr和sin_port的字节顺序都是网络字节顺序。因为他们从报文中读取且要发送到网络。

inet_addr()函数将一个字符串形式的IP地址转换成无符号整形数。eg.

struct sockaddr_in in;
in.sin_addr.s_addr = inet_addr("192.168.1.102");

inet_addr()返回的地址已经是网络字节顺序。
如果inet_addr()函数执行错误,它将会返回–1,二进制的无符号整数值–1相当于255.255.255.255。

inet_ntoa()函数接受一个struct in_addr类型的参数,可以将IP地址转换成字符串。eg.

struct in_addr addrWanIp;
addrWanIp.s_addr = htonl(ip);
char * ipStr = inet_ntoa(addrWanIp); //ipStr就是类似"192.168.1.102"这样的字符串了。

inet_ntoa()是不可重入函数,如果你连续两次调用该函数,就会出问题,因为它的返回值是static的,即第二次返回的结果会把第一次的结果覆盖掉。

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

domain取AF_INET之类的值。type是socket类型,如SOCK_STREAM或SOCK_DGRAM,protocol置0。
返回值为套接字描述符(文件描述符)。错误返回-1,errno将被设置。
domain可以取AF_INET, AF_PACKET, AF_UNIX, AF_NETLINK, AF_PPPOX等,
AF_INET :
SOCK_RAW可以接IPPROTO_UDP和IPPROTO_TCP。
SOCK_STREAM可以接IPPROTO_IP和IPPROTO_TCP。
SOCK_DGRAM可以接IPPROTO_IP和IPPROTO_UDP。
SOCK_PACKET会提醒一次obsolete,并使AF_INET变成AF_PACKET。
AF_PACKET :
type不能是SOCK_STREAM。
SOCK_RAW/SOCK_DGRAM/SOCK_PACKET都可以接IPPROTO_TCP/IPPROTO_UDP/IPPROTO_IP。

在调用socket函数后,套接字的地址格式已经确定了,但尚未给套接字设置本地地址,bind函数用于该目的。

int bind(int sockfd, struct sockaddr * my_addr, int addrlen);

用于将指定socket绑定到机器上的一个端口。第二个参数my_addr是struct sockaddr类型的,包含地址、端口等信息,一般是给struct sockaddr_in类型的变量赋值然后强制转换成struct sockaddr类型,第三个参数就是sizeof(struct sockaddr)。
eg.

struct sockaddr_in my_addr;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(4000);
my_addr.sin_addr.s_addr = inet_addr("166.111.69.52");
bzero(&(my_addr.sin_zero), 8);  /* 将整个结构剩余部分数据设为0 */
bind (sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

如果你想要bind()自动为你选择合适的端口和自动使用本地主机的IP地址,则:

my_addr.sin_port = 0;  /* 随机选择一个端口 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的地址 */

上面两句话写成下面这样比较好,但是INADDR_ANY的值就是0,0的网络和主机字节序是一样的,

my_addr.sin_port = 0;
my_addr.sin_addr.s_addr = INADDR_ANY;

注意,端口要选择1024-65535之间的值,因为Linux(UNIX)中将1-1024的端口作为保留端口,只能由具备root权限的进程使用。
如果并不在乎使用什么端口,就没有必要调用bind函数。

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

参数serv_addr是一个存储远程计算机的IP地址和端口信息的结构。addrlen就是sizeof(struct sockaddr)。
eg.

struct sockaddr_in dest_addr;
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));

当有人连接你的时候,你有两步需要做:通过listen()函数等待连接请求,然后使用accept()函数来处理。

int listen(int sockfd, int backlog); //第二个参数是未经过处理的连接请求队列可以容纳的最大数目。

使用listen就是我们等待别人的连接,所以必须通过bind来指定端口。

有人从很远很远的地方尝试调用connect()来连接你的机器上的某个端口(当然是你已经在listen()的),他的连接将被listen加入等待队列等待accept()函数的调用(加入等待队列的最多数目由listen()函数的第二个参数backlog来决定)。接下来你调用accept()函数,告诉他你接受连接了,accept()函数将回返回一个新的套接字描述符,这个描述符就代表了这个连接。好,这时候你有了两个套接字描述符,返回给你的那个就是和远程计算机的连接,而第一个套接字描述符仍然在你的机器上原来的那个端口上listen()。例如,你在PC1上,PC2和PC3上的客户端程序想连你,通过调用两次accept()就会在listen队列中看到这两个连接请求,accept()返回两个套接字描述符来区分这两个连接,即这时一共有三个套接字描述符存在PC1上。
这时候你所得到的那个新的套接字描述符就可以进行send()操作和recv()操作了。

int accept(int sockfd, void *addr, int *addrlen);

sockfd是正在listen()的套接字描述符,addr一般是一个指向struct sockaddr_in结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的IP地址和端口)。
addrlen是一个本地的整型数值,在它的地址传给accept()前它的值应该是sizeof(struct sockaddr_in),accept()不会在addr中存储多余addrlen bytes大小的数据。如果accept()函数在addr中存储的数据量不足addrlen,则accept()函数会改变addrlen 的值来反应这个情况。
我们使用新的套接字描述符来进行所有的send()和recv()调用,即为每个客户端做区分。如果你只想获得一个单独的连接,那么你可以将原来的sock_fd关掉(调用close()),这样的话就可以阻止以后的连接了。

int send(int sockfd, const void *msg, int len, int flags);

sockfd是代表你与远程程序连接的套接字描述符,即上面accept的一个返回值,msg是一个指针,指向你想发送的信息,如字符串,len是你想发送信息的长度,flags发送标记。一般都设为0。可以注意到这里没有对端的地址信息,因为send是用于发送面向连接的数据的(TCP),只需在连接对端的sockfd上发数据即可。
如果你给send()的参数中包含的数据的长度远远大于send()所能一次发送的数据,则send()函数只发送它所能发送的最大数据长度,所以每次你都要检查send的返回值,如果小于len,就要再发送一次。

int recv(int sockfd, void *buf, int len, unsigned int flags);

sockfd是你要读取数据的套接字描述符,buf指向你存储数据的内存缓存区域,len是缓存区的最大尺寸,flags是recv()函数的一个标志,一般都为0。recv函数返回真正接收的数据长度。

如果想发送和接收SOCK_DGRAM类型或者SOCK_RAW类型的套接字的数据,就要用sendto和recvfrom了,由于是无连接的,所以需要知道对方的IP和端口等信息。

int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

前四个参数和send()相同,参数to是一个指向struct sockaddr结构的指针,里面包含了远程主机的IP地址和端口数据。tolen为sizeof(struct sockaddr)。

int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

如果一个信息大得缓冲区都放不下(大于len),那么多余信息将被砍掉。该调用可以立即返回,也可以永久的等待。这取决于你把flags设置成什么类型。你甚至可以设置超时(timeout)值。
无连接的套接字不需要调用connect(),如果你使用connect()连接到了一个UDP套接字(原本不需要connect的)的服务器程序上,你也可以使用send()和recv()函数来传输你的数据,但这时你所使用的仍然是一个使用着数据报的套接字,只不过套接字在send()和recv()的时候自动帮助你加上了目标地址,目标端口的信息。
UDP使用connect的好处主要是:1. 绑定了目标IP和port,这样只有这个目标发来的消息才会被接收,在一定环境下可以提升安全性。2. 由已连接的UDP套接口引发的异步错误会返回给他们所在的进程,如果不connect,即使sendto发生错误,程序也收不到错误(write或者sendto操作都是把数据放到协议栈的发送队列之后就返回成功,而相应的ICMP回应则要等数据到达对端后才能返回,所以通常这种情况叫做“异步错误”。)。

close(sockfd);

执行close()之后,套接字将不会再允许进行读/写操作。任何有关对套接字描述符进行读和写的操作都会接收到一个错误。

int shutdown(int sockfd, int how);

how可以取下面的值:0表示不允许以后数据的接收,1表示不允许以后数据的发送操作,2表示和close()一样。shutdown()执行成功返回0,失败返回–1。
close和shutdown的区别在网上说法不一,有两点好像是正确的:shutdown用于关闭连接双方的,因此不能用于UDP和RAW,即使用了也不起作用,必须用close;如果多进程共享一个socket,那么close每次只是递减引用计数器,减到0时才真正关闭,想要一下就关闭就得用shutdown。

设置socket选项:

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

sockfd: 已经打开的套接字描述符,level:所使用的协议标准,如套接字标准就是SOL_SOCKET,TCP/IP标准就是IPPROTO_TCP,optname是选项名称,如SO_REUSEADDR,optval是选项的值,optlen是这个值的长度。setsockopt时optval和optlen就是你要赋给optname的值,getsockopt时optval和optlen就存储optname的值。

获取和设置本地主机的主机名,主机名放到name中,len为name的长度。

int gethostname(char *name, size_t len);
int sethostname(const char *name, size_t len);

获取远端的地址信息:参数sockfd是本地的还是远端的?

int getpeername(int sockfd, struct sockaddr *name, int *namelen);

通过主机名或地址信息获取主机的更多信息:

struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, int len, int type);

gethostbyname()一般使用gethostname()获得的主机名,gethostbyaddr()一般使用getpeername()获得的远程主机的地址。注意,如果这两个函数发生错误,不会存到errno中,所以不能perror来获取错误信息,而是通过herror(const char *s),用法和perror相同。另外const char *hstrerror(int err);接收一个h_errno,返回错误信息。

struct hostent
{
  char *h_name;         /* Official name of host.  */
  char **h_aliases;     /* Alias list.  */
  int h_addrtype;       /* Host address type.  */
  int h_length;         /* Length of address.  */
  char **h_addr_list;       /* List of addresses from name server.  */
#define h_addr  h_addr_list[0]  /* Address, for backward compatibility. h_addr_list中的第一个地址  */
};

那我们通过机器名/域名来建立socket连接的方法就是,通过gethostbyname获得hostent结构,然后通过inet_ntoa (((struct in_addr )h->h_addr))就可以获取地址信息了。

server端在bind()之后,就可以通过getsockname()来获取自身的绑定信息,如下:

int serverSockfd;
struct sockaddr_in server_in;
int sin_size = 0;
sin_size = sizeof(struct sockaddr);
getsockname(serverSockfd, (struct sockaddr *)&server_in, &sin_size);

server的自身信息就可以从server_in中获得。

五种I/O模式(6.10节):
1.阻塞I/O:recv和recvfrom是阻塞的,即没有数据到来之前CPU不能做其他事情。
2.非阻塞I/O:recv和recvfrom没有数据时立即返回,但要一直进行recv并轮询返回值,浪费CPU。可以用fcntl来设置非阻塞:
int fcntl(int fd, int cmd, long arg);
例如:
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
3.I/O多路复用:select,没数据时会阻塞,有数据的时候返回就可以进行recv了,它和阻塞I/O的区别是可以同时监听多个套接字描述符上的数据,有一个有数据就返回。而阻塞I/O是阻塞在recv那个地方,recv的时候已经指定某个套接字了。
4.信号驱动I/O(SIGIO):
5.异步I/O:

select()函数:不仅可以用于socket,其他类型的文件描述符也可以用。
可以man select看一下具体参数和例子。

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

numfds是readfds,writefds,exceptfds中fd 集合中文件描述符中最大的数字加上1。
readfds 中的fd集合将由select来监视是否可以读取。
writefds中的fds 集合将由select来监视是否可以写入。
exceptfds中的fds 集合将由select来监视是否有例外发生。
有数据返回非0值,没数据返回0,出错返回-1。

可用宏定义:
FD_ZERO(fd_set *set)将一个文件描述符集合清零。
FD_SET(int fd, fd_set *set)将文件描述符fd 加入集合set中。
FD_CLR(int fd, fd_set *set)将文件描述符fd从集合set中删除。
FD_ISSET(int fd, fd_set *set)测试文件描述符fd是否存在于文件描述符set中,在select返回时可以判断那个fd上有数据了。

struct timeval
{
int tv_sec; /* 秒数 */
int tv_usec; /* 微秒 */
};
因为Linux和UNIX一样,最小的时间片是100微秒,所以不管你将tv_usec 设置的多小,实质上记时器的最小单位是100微秒。

当select()函数返回的时候,timeval 中的时间将会被设置为执行为select()后还剩下的时间。
如果你将struct timeval设置为0,则select()函数将会立即返回,同时返回在你的集合中的文件描述符的状态。
如果你将timeout 这个参数设置为NULL,则select()函数进入阻塞状态,除了等待到文件描述符的状态变化,否则select()函数不会返回。

pselect()和select函数的区别,man select。pselect可以原子性的在select时设置信号屏蔽字,可以同时等待某些信号和某些文件描述符的到来。另外select返回后timeout会变成剩余的时间,pselect不会改变。

poll()函数:和select有什么区别?man poll,epoll。

indefinite:模糊的,不确定的。infinite:无限的。

带外数据(Out-Of-Band data)
有时候在一个网络连接的终端想“快速”的告诉网络另一边的终端一些信息.这个“快速”的意思是我们的某些信息会在正常的网络数据(有时候称为带内数据In-Band data)之前到达网络另一边的终端.这说明,带外数据拥有比一般数据高的优先级.但是不要以为带外数据是通过两条套接字连接来实现的.事实上,带外数据也是通过已有的连接来传输。

远程过程调用:本地的进程来激活远程的进程。相对于本地过程调用。第十二章。
HTTP协议,第十四章。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值