Socket编程小结

来源:互联网 发布:我国教育经费机制知 编辑:程序博客网 时间:2024/03/29 16:10

1. read系统调用

测试程序:客户端向服务器端(tcp)发送一个”hello”字符串,服务器端读取并echo到客户端。

 

服务器端主要代码:

char buf[4096];

int r = tcp_readn(sock, buf, 4096);

int w = tcp_writen(sock, buf, r);

 

客户端主要代码:

char buf[4096];

int w = tcp_writen(sock, “hello”, 5);

int r = tcp_readn(sock, buf, 4096);

 

问题描述:客户端write调用成功,服务器端阻塞在tcp_readn上,tcp_readn的实现如下所示:

 

int tcp_readn(int sock, void* buf, int len)

{

    intrd = 0;

    inti = 0;

    while(rd< len) {

        i= read(sock, (char*)buf + rd, len - rd);

        if(i<= 0) {

            returnrd;

        }

        rd+= i;

    }

    returnrd;

}

 

原因分析:readn必须从sock套接口上读取len个字节,才会返回,不然会一直阻塞;在调试时,我发现tcp_readn中的read执行过一次,读取了5个字节,然后一直阻塞。因为它需要读取4096个字节才返回,将客户端/服务器端中的代码都换成read/write,问题得到解决。

 

附注:read从套接口读取数据,如果缓冲区中有数据已经准备后,read读取缓冲区的数据并返回,read读取的数据量可能比要求的长度要小,但这不能说明read出错,可能是内核中套接口缓冲区中的数据比需要的数据量。如果要判断套接口缓冲区中有多少数据可读或有多大空间可用于写,可通过设置接受和发送低潮标记,分别为SO_RCVLOWAT(缺省值为1)和SO_SNDLOWAT(缺省值为2048),select只有在可读的数据量不低于SO_RCVLOWAT或可写的空间不低于SO_SNDLOWAT时才会返回。

 

 

2. read与write的对应关系

测试程序:客户端调用两次write,服务器端调用一次read。

 

服务器端主要代码:

char buf[4096];

int r = read(sock, buf, 4096);

buf[r] = ‘\0’;

printf(“%s\n”, buf);

 

客户端主要代码:

write(sock, “hello”, 5);

write(sock, “ world”, 6);

 

问题描述:服务器有时打印hello(read对应1个write),有时打印helloworld(read对应2个write)。

 

原因分析:客户端与服务器之间的read/write并没有明确的对应关系。其实read/write只是往套接口缓冲区中读/写数据,数据具体什么时候从缓冲区发送到远端机器的缓冲区是由内核根据TCP的相关原理机制决定的。如果在服务器read读取之前,客户端的两次write的数据都已经到达服务器的套接口缓冲区,则read读取到hello world;否则如果只有第一次write的数据达到缓冲区,则read读取到hello。

 

正常情况下,服务器读取到hello;如在服务器read之前假如sleep(1),则read会读取到helloworld,因为在1s内,两次write的数据都已经到达服务器的缓冲区。

 

3. 值-结果参数

问题描述:accept、recvfrom、getpeername、getsockname不能正确获取对端套接口地址信息。

 

主要代码:

struct sockaddr_in sa;

int sock_len = 0;

recvfrom(sock, buf, len, 0, (struct sockaddr*)&sa,&sock_len);

 

原因分析:套接口函数接受指向套接口地址结构的参数,同时接受地址结构的长度参数,其传递方式决定于传递方式:从进程到内核,还是从内核到进程。

 

1. 从进程到内核,如bind、connect、sendto等,其参数为指向地址结构的指针,以及地址的长度。

2. 从内核到进程,如accept、recvfrom、getsockname、getpeername、其参数为指向地址结构的指针,以及表示结构大小的整数的指针。

其中第二个参数为值-结果参数,当函数调用时,结构大小是一个值,使内核在写结构时不至于越界;但函数返回时,结构大小又是一个结构,它告诉进程内核在此结构中确切存储了多少信息。而代码中sock_len的初值被设置为0,故内核不会往地址结构上写任何信息,sa结构中的内容是随机的,将sock_len的初值设置为sizeof(sa)即可。

 

4. getsockname、getpeername

getsockname、getpeername的调用结果与其调用时机密切相关。具体表现为:

1. TCP服务器端: 在bind以后就可以调用getsockname来获取本地地址和端口getpeername只有在连接建立(accept)以后才调用,否则不能正确获得对方地址和端口。

2. TCP客户端:在调用socket时候内核还不会分配IP和端口,此时调用getsockname不会获得正确的端口和地址(当然链接没建立更不可能调用getpeername),调用了bind 以后可以使用getsockname获取绑定的地址。想要正确的到对方地址(一般客户端不需要这个功能),则必须在链接建立以后,同样链接建立以后,此时客户端地址和端口就已经被指定,此时是调用getsockname的时机。

3. 未连接UDP套接口:在调用connect以后,这2个函数都是可以用的(同样,getpeername也没太大意义。如果你不知道对方的地址和端口,不可能会调用connect)。

4. 已连接UDP套接口(调用connect后): 不能调用getpeername,但是可以调getsockname。和TCP一样,他的地址和端口不是在调用socket就指定了,而是在第一次调用sendto函数以后。

 

5. send/recv与sendto/recvfrom

在TCP中,recv返回值为0表示对端已关闭连接;UDP是无连接的,recvfrom返回为0,说明对端写了一个长度为0的数据报(20字节的ip头部+8字节的UDP头部)。

 

6. TCP/UDP服务器模型

1.TCP的服务模型为并发,而UDP的服务模型为迭代。

2.TCP服务器由监听套接字来接受客户端的请求,当收到请求时,为请求建立新的连接,并可以产生单独的进程(线程)为客户端服务,监听套接字则继续等待新的请求。不同的连接有各自的接受缓冲区,及不同的连接对于TCP服务器来说是独立的。

3.UDP服务器只有一个服务进程,它仅有的单个套接口用于接受所有到达的数据报并发回所有的响应,该套接口有一个接受缓冲区用来存放所到达的数据报。发送给UDP服务器的数据报按顺序进入接收缓冲区,当服务器调用recvfrom时,缓冲区的下一个数据报将返回给进程。

 

7. UDP的connect函数

对于UDP套接口,也可以调用connect,但与TCP不同,UDP的connect过程没有三次握手,内核只是检查是否存在立即可知的错误(如不可达的目的地址),记录对端的IP地址和端口号),然后立即返回到调用进程。

 

对于已连接的UDP套接口,与缺省未连接的UDP套接口相比:

1.不能再为输出操作指定IP地址和端口号,即不能使用sendto,而改用write或send;写到已连接UDP套接口上的任何内容都会自动发送到由connect指定的协议地址。

2.不必使用recvfrom获取数据报的发送者,而改用recv或read。在一个已连接UDP套接口上由内核为输入操作返回的数据报仅仅是那些来自connect所指定协议地址的数据报。这样就限制了一个已连接UDP套接口能且仅能与一个对端交换数据。

3.由已连接UDP套接口引发的异步错误返回给他们所在的进程。

 

未连接UDP套接口发送数据报之前,内核会暂时连接该套接口,并发送数据,然后断开该连接。多个数据报的发送步骤为:

【连接套接口】==》【输出第1个数据报】==》【断开套接口连接】==》

【连接套接口】==》【输出第2个数据报】==》【断开套接口连接】…

【连接套接口】==》【输出第n个数据报】==》【断开套接口连接】

 

当应用程序要给同一目的地址发送多个数据报时,显式连接套接口效率更高,节省了多次向内核拷贝地址开销,其步骤为:

【连接套接口】==》【输出第1个数据报】==》【输出第2个数据报】…

【输出第n个数据报】==》【断开套接口连接】


不管是UDP还是TCP,在调用socket函数获得描述符之后,IP地址和端口都是未知的,这个时候是无法通过getsockname获得这些信息的,取得的IP地址应该是任意的(即0.0.0.0)。如果此时通过bind绑定了端口,端口就确定下来了,绑定端口的同时可以指定任意IP地址(即0.0.0.0),也可以同时指定一个确定的IP地址,那么该socket的地址也是确定的。
不管是UDP还是TCP,在调用socket函数获得描述符之后,都可以马上调用connect函数,这样内核将为该socket指定一个合适的IP地址和临时端口。并且不会改变。
如果UDP没有调用bind或者connect函数,那么将在第一次调用sendto时指定IP地址和临时端口,其中端口是不会改变的,但是IP地址可能发生改变(受到路由的影响,可能选择不同的路径发送数据)。一句话,2次sendto的源IP地址可能是不一样的。在SERVER端看来就好象是2台不同的主机发来的,只不过端口是一样的。