socket套接字

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的 进程通信机制,取后一种意思。通常也称作" 套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的 主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

举例说明

客户端服务端套接字发送演示 客户端服务端套接字发送演示
Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
以J2SDK-1.3为例,Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。

socket现象解释

Socket非常类似于电话插座。以一个国家级电话网为例,电话的通话双方相当于相互通信的2个进程,区号是它的 网络地址;区内一个单位的交换机相当于一台 主机,主机分配给每个用户的局内号码相当于Socket号。任何用户在通话之前,首先要占有一部电话机,相当于申请一个Socket;同时要知道对方的号码,相当于对方有一个固定的Socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。假如对方在场并空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向Socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭Socket,撤消连接。
在电话系统中,一般用户只能感受到本地电话机和对方电话号码的存在,建立通话的过程,话音传输的过程以及整个电话系统的技术细节对他都是透明的,这也与Socket机制非常相似。Socket利用网间网通信设施实现 进程通信,但它对通信设施的细节毫不关心,只要通信设施能提供足够的通信能力,它就满足了。
至此,我们对Socket进行了直观的描述。抽象出来,Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。
在网间网内部,每一个Socket用一个半相关描述:(协议,本地地址,本地端口)。
一个完整的Socket有一个本地唯一的Socket号,由操作系统分配。
最重要的是,Socket是面向客户/ 服务器模型而设计的,针对客户和服务器程序提供不同的Socket 系统调用。客户随机申请一个Socket(相当于一个想打电话的人可以在任何一台入网电话上拨号呼叫),系统为之分配一个Socket号;服务器拥有全局公认的Socket,任何客户都可以向它发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫方知道的电话号码)。
Socket利用客户/服务器模式巧妙地解决了进程之间建立通信连接的问题。服务器Socket半相关为全局所公认非常重要。读者不妨考虑一下,两个完全随机的用户进程之间如何建立通信?假如通信双方没有任何一方的Socket固定,就好比打电话的双方彼此不知道对方的电话号码,要通话是不可能的。

socket连接过程

根据连接启动的方式以及本地 套接字要连接的目标,套接字之间的连接过程可以分为三个步骤: 服务器监听,客户端请求,连接确认。
(1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
(2)客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和 端口号,然后就向服务器端套接字提出连接请求。
(3)连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把 服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端 套接字继续处于 监听状态,继续接收其他客户端套接字的连接请求。

socket常用函数

创建
函数原型:
int socket(int domain, int type, int protocol);
参数说明:
  
domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的 UDP服务应用。
protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。
2.WindowsSocket下 protocol参数中不存在IPPROTO_STCP
  
返回值:
如果调用成功就返回新创建的 套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的 内核缓冲里。
绑定
函数原型:
int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len);
参数说明:
socket:是一个 套接字描述符。
address:是一个sockaddr结构 指针,该结构中包含了要结合的地址和 端口号
address_len:确定address 缓冲区的长度。
返回值:
如果函数执行成功,返回值为0,否则为SOCKET_ERROR。
接收
函数原型:
int recv(SOCKET socket, char FAR* buf, int len, int flags);
参数说明:
  
socket:一个标识已连接 套接口的描述字。
buf:用于接收数据的 缓冲区
len:缓冲区长度。
flags:指定调用方式。取值:MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;MSG_OOB 处理 带外数据
返回值:
若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应 错误代码
函数原型:
ssize_t recvfrom(int sockfd, void buf, int len, unsigned int flags, struct socketaddr* from, socket_t* fromlen);
参数说明:
sockfd:标识一个已连接 套接口的描述字。
buf:接收 数据缓冲区
len:缓冲区长度。
flags:调用操作方式。是以下一个或者多个标志的组合体,可通过or操作连在一起:
(1)MSG_DONTWAIT:操作不会被阻塞;
(2)MSG_ERRQUEUE: 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。
(3)MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
(4)MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对packet套接字有效。
(5)MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
(6)MSG_EOR:指示记录的结束,返回的数据完成一个记录。
(7)MSG_TRUNC:指明数据报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
  
/*(MSG_TRUNC使用错误,4才是MSG_TRUNC的正确解释)*/
(8)MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
(9)MSG_OOB:指示接收到out-of-band数据(即需要优先处理的数据)。
(10)MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。
from:(可选) 指针,指向装有源地址的缓冲区。
fromlen:(可选)指针,指向from缓冲区长度值。
发送
函数原型:
int sendto( SOCKET s, const char FAR* buf, int size, int flags, const struct sockaddr FAR* to, int tolen);
参数说明:
s套接字
buf:待发送数据的缓冲区
size:缓冲区长度
flags:调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
addr:(可选) 指针,指向目的套接字的地址
tolen:addr所指地址的长度
返回值:
如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。
接收连接请求
函数原型:
int accept( int fd, struct socketaddr* addr, socklen_t* len);
参数说明:
fd:套接字描述符。
addr:返回连接着的地址
len:接收返回地址的缓冲区长度
返回值:
成功返回客户端的文件描述符,失败返回-1。

socket实例

JAVA
服务端(Server)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package  com.socket;
 
import  java.io.BufferedReader;
import  java.io.InputStreamReader;
import  java.io.OutputStreamWriter;
import  java.io.Writer;
import  java.net.ServerSocket;
import  java.net.Socket;
import  java.net.SocketException;
import  java.net.SocketTimeoutException;
import  java.text.SimpleDateFormat;
import  java.util.Date;
 
public  class  SocketServer {
     public  static  String _pattern =  "yyyy-MM-dd HH:mm:ss SSS" ;
     public  static  SimpleDateFormat format =  new  SimpleDateFormat(_pattern);
     // 设置超时间
     public  static  int  _sec =  0 ;
 
     public  static  void  main(String[] args) {
         System.out.println( "----------Server----------" );
         System.out.println(format.format( new  Date()));
 
         ServerSocket server;
         try  {
             server =  new  ServerSocket( 8001 );
             System.out.println( "监听建立 等你上线\n" );
 
             Socket socket = server.accept();
             System.out.println(format.format( new  Date()));
             System.out.println( "建立了链接\n" );
 
             BufferedReader br =  new  BufferedReader( new  InputStreamReader(socket.getInputStream()));
 
             socket.setSoTimeout(_sec *  1000 );
             System.out.println(format.format( new  Date()) +  "\n"  + _sec +  "秒的时间 快写\n" );
 
             System.out.println(format.format( new  Date()) +  "\nClient:"  + br.readLine() +  "\n" );
 
             Writer writer =  new  OutputStreamWriter(socket.getOutputStream());
             
             System.out.println(format.format( new  Date()));
             System.out.println( "我在写回复\n" );
             
             writer.write( "收到\n" );
 
             Thread.sleep( 10000 );
             writer.flush();
             
             System.out.println(format.format( new  Date()));
             System.out.println( "写完啦 你收下\n\n\n\n\n" );
         catch  (SocketTimeoutException e) {
             System.out.println(format.format( new  Date()) +  "\n"  + _sec +  "秒没给我数据 我下啦\n\n\n\n\n" );
             e.printStackTrace();
         catch  (SocketException e) {
             e.printStackTrace();
         catch  (Exception e) {
             e.printStackTrace();
         }
     }
}
客户端 (Client)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package  com.socket.v3;
 
import  java.io.BufferedReader;
import  java.io.InputStreamReader;
import  java.io.OutputStreamWriter;
import  java.io.Writer;
import  java.net.Socket;
import  java.net.SocketException;
import  java.net.SocketTimeoutException;
import  java.text.SimpleDateFormat;
import  java.util.Date;
 
public  class  SocketClient {
     public  static  String _pattern =  "yyyy-MM-dd HH:mm:ss SSS" ;
     public  static  SimpleDateFormat format =  new  SimpleDateFormat(_pattern);
     // 设置超时间
     public  static  int  _sec =  5 ;
 
     public  static  void  main(String[] args) {
         System.out.println( "----------Client----------" );
 
         Socket socket =  null ;
         try  {
             // 与服务端建立连接
             socket =  new  Socket( "127.0.0.1" 8001 );
             socket.setSoTimeout(_sec *  1000 );
 
             System.out.println(format.format( new  Date()));
             System.out.println( "建立了链接\n" );
 
             // 往服务写数据
             Writer writer =  new  OutputStreamWriter(socket.getOutputStream());
                         
             System.out.println(format.format( new  Date()));
             System.out.println( "我在写啦\n" );
             Thread.sleep( 10000 );
             writer.write( "有没有收到\n" );
             
             System.out.println(format.format( new  Date()));
             System.out.println( "写完啦 你收下\n" );
             
             writer.flush();
             
             BufferedReader br =  new  BufferedReader( new  InputStreamReader(socket.getInputStream()));
 
             
             System.out.println(format.format( new  Date()) +  "\n"  + _sec +  "秒的时间 告诉我你收到了吗\n" );
 
             System.out.println(format.format( new  Date()) +  "\nServer:"  + br.readLine());
             
         catch  (SocketTimeoutException e) {
             System.out.println(format.format( new  Date()) +  "\n"  + _sec +  "秒没收到回复 我下啦\n\n\n\n\n" );
             e.printStackTrace();
         catch  (SocketException e) {
             e.printStackTrace();
         catch  (Exception e) {
             e.printStackTrace();
         }
     }
}
C
服务端(Server):
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<winsock2.h>
#include<stdio.h>
#pragma comment(lib,"ws2_32.lib")
void  main()
{
WSADATA wsaData;
SOCKET sockServer;
SOCKADDR_IN addrServer;
SOCKET sockClient;
SOCKADDR_IN addrClient;
WSAStartup(MAKEWORD(2,2),&wsaData);
sockServer=socket(AF_INET,SOCK_STREAM,0);
addrServer.sin_addr.S_un.S_addr=htonl(INADDR_ANY); //INADDR_ANY表示任何IP
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(6000); //绑定端口6000
bind(sockServer,(SOCKADDR*)&addrServer, sizeof (SOCKADDR));
 
//Listen监听端
listen(sockServer,5); //5为等待连接数目
printf ( "服务器已启动:\n监听中...\n" );
int  len= sizeof (SOCKADDR);
charsendBuf[100]; //发送至客户端的字符串
charrecvBuf[100]; //接受客户端返回的字符串
 
//会阻塞进程,直到有客户端连接上来为止
sockClient=accept(sockServer,(SOCKADDR*)&addrClient,&len);
//接收并打印客户端数据
recv(sockClient,recvBuf,100,0);
printf ( "%s\n" ,recvBuf);
 
//关闭socket
closesocket(sockClient);
WSACleanup();}
客户端 (Client):
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<winsock2.h>
#include<stdio.h>
#pragma comment(lib,"ws2_32.lib")
void  main()
{
WSADATA wsaData;
SOCKET sockClient; //客户端Socket
SOCKADDR_IN addrServer; //服务端地址
WSAStartup(MAKEWORD(2,2),&wsaData);
//新建客户端socket
sockClient=socket(AF_INET,SOCK_STREAM,0);
//定义要连接的服务端地址
addrServer.sin_addr.S_un.S_addr=inet_addr( "127.0.0.1" ); //目标IP(127.0.0.1是回送地址)
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(6000); //连接端口6000
//连接到服务端
connect(sockClient,(SOCKADDR*)&addrServer, sizeof (SOCKADDR));
//发送数据
char  message[20]= "HelloSocket!" ;
send(sockClient,message, strlen (message)+1,0);
//关闭socket
closesocket(sockClient);
WSACleanup();}
PHP
PHP有强大的Socket操作能力,它的处理方式更接近于C,但是没有C的繁琐。可以看作是对C操作的Socket的一个封装。
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
开启一个socket监听示例程序:
<?php //设置一些基本的变量
$host = "192.168.1.99" ; //主机地址
$port =1234; //端口
//设置超时时间
set_time_limit(0);
//创建一个Socket
$socket =socket_create(AF_INET,SOCK_STREAM,0)ordie( "Couldnotcreatesocket\n" ); //绑定Socket到端口
$result =socket_bind( $socket , $host , $port )ordie( "Couldnotbindtosocket\n" ); //开始监听链接
$result =socket_listen( $socket ,3)ordie( "Couldnotsetupsocketlistener\n" ); //acceptincomingconnections
//另一个Socket来处理通信
$spawn =socket_accept( $socket )ordie( "Couldnotacceptincomingconnection\n" ); //获得客户端的输入
$input =socket_read( $spawn ,1024)ordie( "Couldnotreadinput\n" ); //清空输入字符串
$input =trim( $input ); //处理客户端输入并返回结果
$output = strrev ( $input ). "\n" ;
socket_write( $spawn , $output , strlen ( $output ))ordie( "Couldnotwriteoutput\n" ); //关闭
socket_close( $spawn );
socket_close( $socket );
C#
public  class  XmlSocket
{
//异步socket侦听从客户端传来的数据
public  static  string  data= null ;
//Threadsignal.线程用一个指示是否将初始状态设置为终止的布尔值初始化ManualResetEvent类的新实例。
public  static  ManualResetEvent allDone= new  ManualResetEvent( false );
static  void  Main( string []args)
{
     StartListening();
}
public  static  void  StartListening()
{
//传入数据缓冲
byte [] bytes =  new  Byte[1024];
//建立本地端口
IPAddress ipAddress;
String ipString = ConfigurationManager.AppSettings.Get( "SocketIP" );
if (ipString ==  null  || ipString == String.Empty)
{
IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
ipAddress = ipHostInfo.AddressList[0];
}
else
{
ipAddress = IPAddress.Parse(ipString);
}
int  port;
String portString = ConfigurationManager.AppSettings.Get( "SocketPort" );
if (portString ==  null  || portString == String.Empty)
{
port=11001;
}
else
{
port= int .Parse(portString);
}
IPEndPoint localEndPoint =  new  IPEndPoint(ipAddress, port);
Socket listener =  new  Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//绑定端口和数据
listener.Bind(localEndPoint);
listener.Listen(100);
while ( true )
{ //设置无信号状态的事件
allDone.Reset();
//重启异步连接
listener.BeginAccept( new  AsyncCallback(AcceptCallback),listener);
//等待连接创建后继续
allDone.WaitOne();
}
public  static  void  AcceptCallback(IAsyncResult ar)
{
//接受回调方法。该方法的此节向主应用程序线程发出信号,让它继续处理并建立与客户端的连接
allDone.Set();
//获取客户端请求句柄
Socket listener = (Socket)ar.AsyncState;
Socket handler = listener.EndAccept(ar);
StateObject state =  new  StateObject();
state.workSocket = handler;
handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,  new  AsyncCallback(ReadCallback),state);
}
//与接受回调方法一样,读取回调方法也是一个AsyncCallback委托。该方法将来自客户端套接字的一个或多个字节读入数据缓冲区,然后再次调用BeginReceive方法,直到客户端发送的数据完成为止。从客户端读取整个消息后,在控制台上显示字符串,并关闭处理与客户端的连接的服务器套接字。
public  static  void  ReadCallback(IAsyncResult ar)
{
String content = String.Empty;
//创建自定义的状态对象
StateObject state = (StateObject)ar.AsyncState;
Socket handler = state.workSocket; //处理的句柄
//读出
int  bytesRead = handler.EndReceive(ar);
if (bytesRead>0)
{
String len = Encoding.UTF8.GetBytes(result).Length.ToString().PadLeft(8, '0' );
log.writeLine(len);
Send(len + result, handler);
}
}
private  static  void  Send( string  data, Socket handler)
{
byte [] byteData = Encoding.UTF8.GetBytes(data);
handler.BeginSend(byteData, 0, byteData.Length, 0,  new  AsyncCallback(SendCallback), handler);
}
private  static  void  SendCallback(IAsyncResult ar)
{
Socket handler = (Socket)ar.AsyncState;
//向远端发送数据
int  bytesSent = handler.EndSend(ar);
StateObject state =  new  StateObject();
state.workSocket = handler;
handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,  new  AsyncCallback(ReadCallback),state);
handler.Shutdown(SocketShutdown.Both);
handler.Close();
}
public  static  void  StopListening()
{
allDone.Close();
log.close();
}

socket插槽类型

Socket AM2,原称“Socket M2”,是供 AMD桌上型处理器使用的CPU插座,用以取代 Socket 754和939,并已于2006年5月23日推出。它拥有940针,支援双通道 DDR2 SDRAM,但针脚的排列方式与 Socket 940不相同,又因为S940不支持DDR2 SDRAM,因此两者并不兼容。 [1]  
LGA 1150 插槽,是 Intel 即将发布的第八代产品所采用的插槽,支持CPU新核心 Haswell ,不兼容原有 IVB、SNB 核心的CPU,新版将于2013年发布。
LGA 1155 [2]   插槽,是 Intel 平台 7/6 系列主板采用的插槽,即 Intel Ivy Bridge 核心、Sandy Bridge(新Celeron、新Pentium、第二、三代 i3/i5/i7 处理器采用)。
LGA 2011 [3]   ,又称Socket R,是 英特尔(Intel)Sandy Bridge-EX微架构 CPU所使用的CPU接口。LGA2011接口将取代LGA1366接口,成为Intel最新的 旗舰产品。
LGA2011接口有2011个触点,将包含以下新特性:
1、处理器最高可达八核。
2、支持四通道 DDR3内存
3、支持PCI-E 3.0规范。
4、 芯片组使用单芯片设计,支持两个SATA 3Gbps和多达十个SATA/SAS 6Gbps接口。
LGA 1567,又称为Socket LS,是Intel所推出的处理器插座,用于多路(多处理器)服务器上。其插座有1567个金属接触针脚,对应处理器上有1567个金属触点。于 2010 年 3 月发布,基于Intel Nehalem架构,核心代号“Beckton”的Intel Xeon-7500系列和Intel Xeon-6500系列的处理器。
Socket 939插槽,是Athlon64处理器所采用的接口类型, 针脚数为939针。支持 Socket 939 处理器的主板只需要4层 PCB。使用普通DDR内存。
Socket 940插槽,是Athlon64处理器所采用的接口类型,针脚数为940针。Socket 940接口的处理器支持双通道ECC内存,支持 Socket 940 处理器的主板必须采用6至9层PCB,必须采用带ECC校验的DDR内存。
Socket 754插槽,是Athlon64处理器所采用的接口类型,针脚数为754针。Socket 754 接口处理器支持单通道内存
LGA 775插槽,是Intel 925X Express和Intel 915 Express 芯片组,所采用的接口类型,支持Pentium 4和Pentium 4 Extreme Edition处理器, 针脚数为775针。
Socket 478插槽是旧型号Pentium 4系列处理器所采用的接口类型,针脚数为478针。Socket 478的Pentium 4处理器面积很小,其 针脚排列极为紧密。采用Socket 478插槽的主板产品数量众多,是20世纪应用最为广泛的插槽类型。
Socket A接口,也叫Socket 462,是目前AMD公司Athlon XP和Duron处理器的插座标准。Socket A接口具有462插空,可以支持133MHz 外频。如同Socket 370一样,降低了制造成本,简化了结构设计。
Socket 423插槽是最初Pentium 4处理器的标准接口,Socket 423的外形和前几种Socket类的插槽类似,对应的CPU 针脚数为423。Socket 423插槽多是基于Intel 850芯片组主板,支持1.3GHz~1.8GHz的Pentium 4处理器。不过随着DDR内存的流行, 英特尔又开发了支持SDRAM及DDR内存的i845 芯片组,CPU插槽也改成了Socket 478,Socket 423插槽也就销声匿迹了。
Socket 370架构是英特尔开发出来代替SLOT架构,外观上与Socket 7非常像,也采用零插拔力插槽,对应的CPU是370 针脚
Socket 370主板多为采用Intel ZX、BX、i810芯片组的产品,其他厂商有VIA Apollo Pro系列、SIS 530系列等。最初认为,Socket 370的CPU升级能力可能不会太好,所以Socket 370的销量总是不如SLOT 1接口的主板。但在 英特尔推出的“铜矿”和”图拉丁”系列CPU, Socket 370接口的主板一改低端形象,逐渐取代了SLOT 1接口。目前市场中还有极少部分的主板采用此种插槽。
SLOT 1是英特尔公司为取代Socket 7而开发的CPU接口,并申请的专利。这样其它厂商就无法生产SLOT 1接口的产品,也就使得AMD、VIA、SIS等公司不得不联合起来,对Socket 7接口升级,也得到了Super 7接口。后来随着Super 7接口的兴起,英特尔又将SLOT 1结构主板的制造授权提供给了VIA、SIS、ALI等主板厂商,所以这些厂商也相应推出了采用SLOT 1接口的系列主板,丰富了主板市场。
SLOT 1是 英特尔公司为Pentium Ⅱ系列CPU设计的插槽,其将Pentium Ⅱ CPU及其相关控制电路、 二级缓存都做在一块子卡上,多数Slot 1主板使用100MHz 外频。SLOT 1的技术结构比较先进,能提供更大的内部传输 带宽和CPU性能。采用SLOT 1接口的主板芯片组有Intel的BX、i810、i820系列及VIA的Apollo系列,ALI 的Aladdin Pro Ⅱ系列及SIS的620、630系列等。此种接口已经被淘汰,市面上已无此类接口的主板产品。
SLOT 2用途比较专业,都采用于高端 服务器图形工作站的系统。所用的CPU也是很昂贵的Xeon( 至强)系列。Slot 2与Slot 1相比,有许多不同。首先,Slot 2插槽更长,CPU本身也都要大一些。其次,Slot 2能够胜任更高要求的多用途计算处理,这是进入高端企业计算市场的关键所在。在当时标准服务器设计中,一般厂商只能同时在系统中采用两个 Pentium Ⅱ处理器,而有了Slot 2设计后,可以在一台服务器中同时采用 8个处理器。而且采用Slot 2接口的Pentium Ⅱ CPU都采用了当时最先进的0.25微米制造工艺。支持SLOT 2接口的主板芯片组有440GX和450NX。
SLOT A接口类似于英特尔公司的SLOT 1接口,供AMD公司的K7 Athlon使用的。在技术和性能上,SLOT A主板可完全兼容原有的各种外设扩展卡设备。它使用的并不是Intel的P6 GTL+总线协议,而是Digital公司的Alpha总线协议EV6。EV6架构是种较先进的架构,它采用多线程处理的点到点 拓扑结构,支持200MHz的 总线频率。支持SLOT A接口结构的主板 芯片组主要有两种,一种是AMD的AMD 750芯片组,另一种是VIA的Apollo KX133芯片组。此类接口已被Socket A接口全面取代。
Socket 7:Socket在英文里就是插槽的意思,Socket 7也被叫做Super 7。最初是 英特尔公司为Pentium MMX系列CPU设计的插槽,后来英特尔放弃Socket 7接口转向SLOT 1接口,AMD、VIA、ALI、SIS等厂商仍然沿用此接口,直至发展出Socket A接口。该插槽基本特征为321插孔,系统使用66MHz的总线。Super 7主板增加了对100MHz 外频和AGP接口类型的支持。
Super 7采用的 芯片组有VIA公司的MVP3、MVP4系列,SIS公司的530/540系列及ALI的Aladdin V系列等主板产品。对应Super 7接口CPU的产品有AMD K6-2、K6-Ⅲ 、Cyrix M2及一些其他厂商的产品。此类接口目前已被淘汰,只有部分老产品才能见到。
UNIX system V有:消息(message)、共享存储区 (shared memory)和信号量(semaphore)等.
网络层的“ ip地址 可以唯一标识网络中的主机,而 传输层的“ 协议+端口 可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX  BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。
为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。


SOCKET: 插座
      TCP/IP协议存在于OS中,网络服务通过OS提供, 在OS中增加支持TCP/IP的系统调用——Berkeley套接字, 如Socket,Connect,Send,Recv等

“一切皆Socket!”

话虽些许夸张,但是事实也是,现在的网络编程几乎都是用的socket。

——有感于实际编程和开源项目研究。

我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的。本文的主要内容如下:

  • 1、网络中进程之间如何通信?
  • 2、Socket是什么?
  • 3、socket的基本操作
    • 3.1、socket()函数
    • 3.2、bind()函数
    • 3.3、listen()、connect()函数
    • 3.4、accept()函数
    • 3.5、read()、write()函数等
    • 3.6、close()函数
  • 4、socket中TCP的三次握手建立连接详解
  • 5、socket中TCP的四次握手释放连接详解
  • 6、一个例子(实践一下)
  • 7、留下一个问题,欢迎大家回帖回答!!!

1、网络中进程之间如何通信?

本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名的和具名的)
  • 远程过程调用(Solaris门和Sun RPC)

但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址可以唯一标识网络中的主机,而传输层的“协议+端口可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX  BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。

2、什么是Socket?

上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

socket一词的起源

在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”

3、socket的基本操作

既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

3.1、socket()函数

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

调用该函数前,先要加载winsock,wsastartup().socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

如果成功就返回生成的SOCKET,如果失败就返回INVALID_SOCKET(-1).

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

  • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INETAF_INET6AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定socket类型。常用的socket类型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等(socket的类型有哪些?)。
    类型 解释
    SOCK_STREAM 提供有序的、可靠的、双向的和基于连接的 字节流,使用 带外数据传送机制,为Internet地址族使用TCP。
    SOCK_DGRAM 支持无连接的、不可靠的和使用固定大小(通常很小)缓冲区的 数据报服务,为Internet地址族使用UDP。
    SOCK_STREAM类型的套接口为全双向的字节流。对于流类套接口,在接收或发送数据前必需处于已连接状态。用 connect()调用建立与另一套接口的连接,连接成功后,即可用 send()和recv()传送数据。当会话结束后,调用closesocket()。 带外数据根据规定用send()和recv()来接收。                                                                                                                 原始套接字(SOCK_RAW):原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据包套接字)的 区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据包套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。
    实现SOCK_STREAM类型 套接口的通讯协议保证数据不会丢失也不会重复。如果终端协议有缓冲区空间,且数据不能在一定时间成功发送,则认为连接中断,其后续的调用也将以WSAETIMEOUT错误返回。
    SOCK_DGRAM类型套接口允许使用 sendto()和recvfrom()从任意端口发送或接收数据报。如果这样一个套接口用 connect()与一个指定端口连接,则可用 send()和recv()与该端口进行数据报的发送与接收。
  • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。                                                                                

    1 参数protocol用来指明所要接收的协议包,如果是象IPPROTO_TCP(6)这种非0、非255的协议,当操作系统内核碰到ip头中protocol域和创建socket所使用参数protocol相同的IP包,就会交给这个raw socket来处理,因此,一般来说,要想接收什么样的数据包,就应该在参数protocol里来指定相应的协议。当内核向此raw socket交付数据包的时候,是包括整个IP头的,并且已经是重组好的IP包。
    2 如果protocol是IPPROTO_RAW(255),这时候,这个socket只能用来发送IP包,而不能接收任何的数据。发送的数据需要自己填充IP包头,并且自己计算校验和。
    3 对于protocol为0(IPPROTO_IP)的raw socket。用于接收任何的IP数据包。其中的校验和和协议分析由程序自己完成。  

    这是include/Linux/in.h里的定义:

    /* Standard well-defined IP protocols. */
    enum {
    IPPROTO_IP = 0,   /* Dummy protocol for TCP   */
    IPPROTO_ICMP = 1,   /* Internet Control Message Protocol */
    IPPROTO_IGMP = 2,   /* Internet Group Management Protocol */
    IPPROTO_IPIP = 4,   /* IPIP tunnels (older KA9Q tunnels use 94) */
    IPPROTO_TCP = 6,   /* Transmission Control Protocol */
    IPPROTO_EGP = 8,   /* Exterior Gateway Protocol   */
    IPPROTO_PUP = 12,   /* PUP protocol     */
    IPPROTO_UDP = 17,   /* User Datagram Protocol   */
    IPPROTO_IDP = 22,   /* XNS IDP protocol    */
    IPPROTO_DCCP = 33,   /* Datagram Congestion Control Protocol */
    IPPROTO_RSVP = 46,   /* RSVP protocol    */
    IPPROTO_GRE = 47,   /* Cisco GRE tunnels (rfc 1701,1702) */

    IPPROTO_IPV6 = 41,   /* IPv6-in-IPv4 tunnelling   */

    IPPROTO_ESP = 50,            /* Encapsulation Security Payload protocol */
    IPPROTO_AH = 51,             /* Authentication Header protocol       */
    IPPROTO_BEETPH = 94,        /* IP option pseudo header for BEET */
    IPPROTO_PIM    = 103,   /* Protocol Independent Multicast */

    IPPROTO_COMP   = 108,                /* Compression Header protocol */
    IPPROTO_SCTP   = 132,   /* Stream Control Transport Protocol */
    IPPROTO_UDPLITE = 136, /* UDP-Lite (RFC 3828)    */

    IPPROTO_RAW = 255,   /* Raw IP packets    */
    IPPROTO_MAX
    };

               注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址,其实在调用socket函数创建socket时,内核还并未给socket分配源地址和源端口。而对于UDP,我猜测在调用sendto发送数据时,在未捆绑端口的情况下,内核也会随机分配端口。。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()listen()时系统会自动随机分配一个端口。

3.2、bind()函数

正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INETAF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数的三个参数分别为:

  • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
  • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: 
    struct sockaddr_in {
        sa_family_t    sin_family; /* address family: AF_INET */
        in_port_t      sin_port;   /* port in network byte order */
        struct in_addr sin_addr;   /* internet address */
    };
    
    /* Internet address. */
    struct in_addr {
        uint32_t       s_addr;     /* address in network byte order */
    };
    ipv6对应的是: 
    struct sockaddr_in6 { 
        sa_family_t     sin6_family;   /* AF_INET6 */ 
        in_port_t       sin6_port;     /* port number */ 
        uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
        struct in6_addr sin6_addr;     /* IPv6 address */ 
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
    };
    
    struct in6_addr { 
        unsigned char   s6_addr[16];   /* IPv6 address */ 
    };
    Unix域对应的是: 
    #define UNIX_PATH_MAX    108
    
    struct sockaddr_un { 
        sa_family_t sun_family;               /* AF_UNIX */ 
        char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
    };
  • addrlen:对应的是地址的长度。

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

     在《UNIX网络编程》这本书中提到:“如果一个TCP客户或者服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时接口。”从这句话中可以判断出,其实在调用socket函数创建socket时,内核还并未给socket分配源地址和源端口。而对于UDP,我猜测在调用sendto发送数据时,在未捆绑端口的情况下,内核也会随机分配端口。
  而我遇到的特殊应用要求我在用UDP发送数据之前要告诉对方我的发送端口,这也就意味着我在sendto之前必须要捆绑端口,因此我在发送数据之前就得调用bind函数绑定一下端口了。但是我就在想内核既然有随机分配端口的能力,而我需要的也只是让它绑定一下而不用绑定在固定端口的业务,socket中应该能够提供这种业务。然后果然我发现bind就具备这种能力,当bind的参数中端口地址为0的时候,这时候就是由内核分配端口。这样我就不用考虑端口地址重复的问题,而放心的把这个问题交给内核处理了。
  就在发现bind的这个机制的同时,我发现其实bind对于源地址也同样具备这种处理方式,当系统具有多IP(多网卡)的情况,当我们把bind函数中的ip参数置0时,就是由内核自己选择分配IP。而之前一直觉得很神奇的INADDR_ANY其实一点也不神奇,它的值其实就是0。所以当我们只有单一IP的时候,我们就可以用INADDR_ANY去代替那个单一的IP,因为内核分配的时候只能选择这一个IP。从而造成了INADDR_ANY就是本机IP的现象。

网络字节序与主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。

3.3、listen()、connect()函数

如果作为一个服务器,在调用socket()bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

3.4、accept()函数

TCP服务器端依次调用socket()bind()listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

3.5、read()、write()等函数

万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

       #include <unistd.h>

       ssize_t read(int fd, void *buf, size_t count);
       ssize_t write(int fd, const void *buf, size_t count);

       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);
       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。

3.6、close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include <unistd.h>
int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

4、socket中TCP的三次握手建立连接详解

我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

  • 客户端向服务器发送一个SYN J
  • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
  • 客户端再想服务器发一个确认ACK K+1

只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

image

图1、socket中发送的TCP三次握手

从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。

5、socket中TCP的四次握手释放连接详解

上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:

image

图2、socket中发送的TCP四次握手

图示过程如下:

  • 某个应用进程首先调用 close主动关闭连接,这时TCP发送一个FIN M;
  • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
  • 一段时间之后,接收到文件结束符的应用进程调用 close关闭它的socket。这导致它的TCP也发送一个FIN N;
  • 接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

6、一个例子(实践一下)

说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。

服务器端代码:

服务器端
复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    listenfd, connfd;
    struct sockaddr_in     servaddr;
    char    buff[4096];
    int     n;

    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
    printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666);

    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
    printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }

    if( listen(listenfd, 10) == -1){
    printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }

    printf("======waiting for client's request======\n");
    while(1){
    if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
        continue;
    }
    n = recv(connfd, buff, MAXLINE, 0);
    buff[n] = '\0';
    printf("recv msg from client: %s\n", buff);
    close(connfd);
    }

    close(listenfd);
}
复制代码

客户端代码:

复制代码

 
#include < stdio.h > #include < stdlib.h > #include < string .h > #include < errno.h > #include < sys / types.h > #include < sys / socket.h > #include < netinet / in .h > #define MAXLINE 4096 int main( int argc, char ** argv) { int sockfd, n; char recvline[ 4096 ], sendline[ 4096 ]; struct sockaddr_in servaddr; if ( argc != 2 ){ printf( " usage: ./client <ipaddress>\n " ); exit( 0 ); } if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ){ printf( " create socket error: %s(errno: %d)\n " , strerror(errno),errno); exit( 0 ); } memset( & servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons( 6666 ); if ( inet_pton(AF_INET, argv[ 1 ], & servaddr.sin_addr) <= 0 ){ printf( " inet_pton error for %s\n " ,argv[ 1 ]); exit( 0 ); } if ( connect(sockfd, ( struct sockaddr * ) & servaddr, sizeof (servaddr)) < 0 ){ printf( " connect error: %s(errno: %d)\n " ,strerror(errno),errno); exit( 0 ); } printf( " send msg to server: \n " ); fgets(sendline, 4096 , stdin); if ( send(sockfd, sendline, strlen(sendline), 0 ) < 0 ) { printf( " send msg error: %s(errno: %d)\n " , strerror(errno), errno); exit( 0 ); } close(sockfd); exit( 0 ); }
复制代码

当然上面的代码很简单,也有很多缺点,这就只是简单的演示socket的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要fork()一个新的进程或者线程去处理请求等。

windows下:

服务端:

// sockettest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>  
#include<cstdlib>
#include<cstring>
#include<cerrno>
#include<sys/types.h>
#include<winsock2.h>
//#include<netdb.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;  

#define MAXSIZE 4096
int _tmain(int argc, _TCHAR* argv[])
{
	WORD requestVersion=MAKEWORD(2,2);
	WSADATA wsadata;
	int err=WSAStartup(requestVersion,&wsadata);
	if (err!=0)
	{
		printf("WSAStartup failed witherror: %d\n", err);
		return 1;
	}

	int listenfd,connfd;
	sockaddr_in servaddr;
	char buff[4096];
	memset(buff,0,sizeof(char)*4096);
	int n;
	if ((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
	{
		char tmp[4096]={0};
		strerror_s(tmp,errno);
		cout<<"create socket error:";
		printf("%s",tmp);
		cout<<" : "<<errno<<endl;
		exit(0);
	}
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_port=htons(6666);
	servaddr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
	servaddr.sin_family=AF_INET;
	if (bind(listenfd,(sockaddr*)&servaddr,sizeof(servaddr)))
	{
		char tmp[4096]={0};
		strerror_s(tmp,errno);
		cout<<"bind socket error:";
		printf("%s",tmp);
		cout<<" : "<<errno<<endl;
		exit(0);
	}
	if (listen(listenfd,10)==-1)
	{
		char tmp[4096]={0};
		strerror_s(tmp,errno);
		cout<<"listen socket error:";
		printf("%s",tmp);
		cout<<" : "<<errno<<endl;
		exit(0);
	}
	cout<<"==========waiting for client's request====================="<<endl;
	while(1)
	{
		if ((connfd=accept(listenfd,(sockaddr*)NULL,NULL))==-1)
		{
			char tmp[4096]={0};
		strerror_s(tmp,errno);
		cout<<"accept socket error:";
		printf("%s",tmp);
		cout<<" : "<<errno<<endl;
		continue;
		}
		memset(buff,0,sizeof(buff));
		n=recv(connfd,buff,MAXSIZE,0);
		buff[n]='\0';
		cout<<"say:";
		printf("%s",buff);
		cout<<endl;
		closesocket(connfd);
	}
	closesocket(listenfd);
	return 0;
}


客户端:

// ClientSocket.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>
#include<cstring>
#include<cerrno>
#include<sys/types.h>
#include<winsock2.h>
#pragma comment(lib,"ws2_32")
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
	SOCKET sockfd,n;
	char recvline[4096],sendline[4096];
	sockaddr_in servaddr;
	/*if (argc!=2)
	{
		cout<<"usage: ./client <ipaddress>\n";
		exit(0);
	}*/
	WORD wRequestVersion=MAKEWORD(2,2);
	WSADATA wsadata;
	if (WSAStartup(wRequestVersion,&wsadata)!=0)
	{
		cout<<"socket initial failed"<<endl;
		exit(0);
	}
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family=AF_INET;
	servaddr.sin_addr.S_un.S_addr=inet_addr("10.129.119.207");
	servaddr.sin_port=htons(6666);
	cout<<"=============send msg to server========="<<endl;
	while(1)
	{
	if ((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
	{
		cout<<"create socket failed"<<endl;
		exit(0);
	}
	if (connect(sockfd,(sockaddr*)&servaddr,sizeof(servaddr))<0)
	{
		cout<<"connect failed"<<endl;
		cout<<WSAGetLastError();
		exit(0);
	}
    memset(sendline,0,sizeof(sendline));
	fgets(sendline,4096,stdin);
	sendline[strlen(sendline)-1]=0;
	if (strcmp(sendline,"exit")==0)
		break;
	if (send(sockfd,sendline,strlen(sendline),0)<0)
	{
		cout<<"send msg failed"<<endl;
		cout<<WSAGetLastError();
		exit(0);
	}
	}
	closesocket(sockfd);
	return 0;
}



7、动动手

留下一个问题,欢迎大家回帖回答!!!是否熟悉Linux下网络编程?如熟悉,编写如下程序完成如下功能:

服务器端:

接收地址192.168.100.2的客户端信息,如信息为“Client Query”,则打印“Receive Query”

客户端:

向地址192.168.100.168的服务器端顺序发送信息“Client Query test”,“Cleint Query”,“Client Query Quit”,然后退出。

题目中出现的ip地址可以根据实际情况定。

——本文只是介绍了简单的socket编程。

更为复杂的需要自己继续深入。

(unix domain socket)使用udp发送>=128K的消息会报ENOBUFS的错误(一个实际socket编程中遇到的问题,希望对你有帮助)

作者:吴秦
出处:http://www.cnblogs.com/skynet/


©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页