├── .gitignore ├── Makefile ├── README.md ├── bin ├── ditaa0_9.jar ├── ditaa0_9_gbk.diff ├── ditaa0_9_gbk.jar ├── gitdown ├── sflcvdp.pl └── sfldate.pl ├── chapter1.md ├── chapter1.txt ├── chapter2.md ├── chapter2.txt ├── chapter3.md ├── chapter3.txt ├── chapter4.md ├── chapter4.txt ├── chapter5.md ├── chapter5.txt └── images ├── chapter1_1.png ├── chapter1_2.png ├── chapter1_3.png ├── chapter1_4.png ├── chapter1_5.png ├── chapter1_6.png ├── chapter1_7.png ├── chapter1_8.png ├── chapter1_9.png ├── chapter2_1.png ├── chapter2_10.png ├── chapter2_11.png ├── chapter2_12.png ├── chapter2_13.png ├── chapter2_14.png ├── chapter2_15.png ├── chapter2_16.png ├── chapter2_17.png ├── chapter2_18.png ├── chapter2_2.png ├── chapter2_3.png ├── chapter2_4.png ├── chapter2_5.png ├── chapter2_6.png ├── chapter2_7.png ├── chapter2_8.png ├── chapter2_9.png ├── chapter3_1.png ├── chapter3_10.png ├── chapter3_11.png ├── chapter3_12.png ├── chapter3_13.png ├── chapter3_14.png ├── chapter3_15.png ├── chapter3_16.png ├── chapter3_17.png ├── chapter3_18.png ├── chapter3_19.png ├── chapter3_2.png ├── chapter3_20.png ├── chapter3_21.png ├── chapter3_22.png ├── chapter3_23.png ├── chapter3_24.png ├── chapter3_25.png ├── chapter3_26.png ├── chapter3_3.png ├── chapter3_4.png ├── chapter3_5.png ├── chapter3_6.png ├── chapter3_7.png ├── chapter3_8.png ├── chapter3_9.png ├── chapter4_1.png ├── chapter4_2.png ├── chapter4_3.png ├── chapter4_4.png ├── chapter4_5.png ├── chapter4_6.png ├── chapter4_7.png ├── chapter4_8.png ├── chapter4_9.png ├── chapter5_1.png ├── chapter5_2.png ├── chapter5_3.png ├── chapter5_4.png ├── chapter5_5.png ├── chapter5_6.png └── chapter5_7.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GITDOWN=bin/gitdown 2 | 3 | CHAPTERS=chapter1.md chapter2.md chapter3.md chapter4.md chapter5.md 4 | 5 | all: $(CHAPTERS) 6 | 7 | %.md: %.txt 8 | $(GITDOWN) $*.txt 9 | 10 | clean: 11 | rm -f $(CHAPTERS) 12 | rm -fr images 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZMQ 指南 2 | 3 | **作者: Pieter Hintjens , CEO iMatix Corporation.** 4 | 5 | **原文地址: https://github.com/imatix/zguide/tree/v2.2** 6 | 7 | **翻译: 张吉 , 安居客集团 好租网工程师** 8 | 9 | **NOTE**: 此翻译涵盖2011年10月份的ZMQ稳定版本,即2.1.0 stable release。但读者仍然可以通过此文了解ZMQ的一些基本概念和哲学。 10 | 11 | --- 12 | 13 | * [第一章 ZeroMQ基础][1] 14 | * [第二章 ZeroMQ进阶][2] 15 | * [第三章 高级请求-应答模式][3] 16 | * [第四章 可靠的请求-应答模式][4] 17 | * [第五章 高级发布-订阅模式][5] 18 | 19 | 20 | [1]: /chapter1.md 21 | [2]: /chapter2.md 22 | [3]: /chapter3.md 23 | [4]: /chapter4.md 24 | [5]: /chapter5.md 25 | 26 | --- 27 | 28 | This work is licensed under a [Creative Commons Attribution-ShareAlike 3.0 License](http://creativecommons.org/licenses/by-sa/3.0/). 29 | 30 | -------------------------------------------------------------------------------- /bin/ditaa0_9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/bin/ditaa0_9.jar -------------------------------------------------------------------------------- /bin/ditaa0_9_gbk.diff: -------------------------------------------------------------------------------- 1 | Index: src/org/stathissideris/ascii2image/graphics/Diagram.java 2 | =================================================================== 3 | --- src/org/stathissideris/ascii2image/graphics/Diagram.java (revision 80) 4 | +++ src/org/stathissideris/ascii2image/graphics/Diagram.java (working copy) 5 | @@ -30,6 +30,7 @@ 6 | import org.stathissideris.ascii2image.core.Pair; 7 | import org.stathissideris.ascii2image.text.AbstractionGrid; 8 | import org.stathissideris.ascii2image.text.CellSet; 9 | +import org.stathissideris.ascii2image.text.StringUtils; 10 | import org.stathissideris.ascii2image.text.TextGrid; 11 | import org.stathissideris.ascii2image.text.TextGrid.Cell; 12 | import org.stathissideris.ascii2image.text.TextGrid.CellColorPair; 13 | @@ -42,8 +43,8 @@ 14 | */ 15 | public class Diagram { 16 | 17 | - private static final boolean DEBUG = true; 18 | - private static final boolean DEBUG_VERBOSE = true; 19 | + private static final boolean DEBUG = false; 20 | + private static final boolean DEBUG_VERBOSE = false; 21 | private static final boolean DEBUG_MAKE_SHAPES = false; 22 | 23 | private ArrayList shapes = new ArrayList(); 24 | @@ -544,7 +545,8 @@ 25 | String string = pair.string; 26 | if (DEBUG) 27 | System.out.println("Found string "+string); 28 | - TextGrid.Cell lastCell = isolationGrid.new Cell(cell.x + string.length() - 1, cell.y); 29 | + //TextGrid.Cell lastCell = isolationGrid.new Cell(cell.x + string.length() - 1, cell.y); 30 | + TextGrid.Cell lastCell = isolationGrid.new Cell(cell.x + StringUtils.GBKLength(string) - 1, cell.y); 31 | 32 | int minX = getCellMinX(cell); 33 | int y = getCellMaxY(cell); 34 | Index: src/org/stathissideris/ascii2image/text/StringUtils.java 35 | =================================================================== 36 | --- src/org/stathissideris/ascii2image/text/StringUtils.java (revision 80) 37 | +++ src/org/stathissideris/ascii2image/text/StringUtils.java (working copy) 38 | @@ -20,6 +20,8 @@ 39 | */ 40 | package org.stathissideris.ascii2image.text; 41 | 42 | +import java.nio.charset.Charset; 43 | + 44 | /** 45 | * @author sideris 46 | * 47 | @@ -28,6 +30,10 @@ 48 | */ 49 | public class StringUtils { 50 | 51 | + public static int GBKLength(String str) { 52 | + return str.getBytes(Charset.forName("GBK")).length; 53 | + } 54 | + 55 | /** 56 | * The indexOf idiom 57 | * 58 | Index: src/org/stathissideris/ascii2image/text/TextGrid.java 59 | =================================================================== 60 | --- src/org/stathissideris/ascii2image/text/TextGrid.java (revision 80) 61 | +++ src/org/stathissideris/ascii2image/text/TextGrid.java (working copy) 62 | @@ -22,6 +22,7 @@ 63 | 64 | import java.awt.Color; 65 | import java.io.*; 66 | +import java.nio.charset.Charset; 67 | import java.util.*; 68 | import java.util.regex.Matcher; 69 | import java.util.regex.Pattern; 70 | @@ -1563,6 +1564,26 @@ 71 | byte[] bytes = row.getBytes(); 72 | row = new String(bytes, encoding); 73 | } 74 | + 75 | + StringBuilder builder = new StringBuilder(row.length()); 76 | + 77 | + int q = 0; 78 | + for (int p = 0; p < row.length(); p++) { 79 | + char ch = row.charAt(p); 80 | + if (StringUtils.isOneOf(ch, boundaries) 81 | + || StringUtils.isOneOf(ch, cornerChars)) { 82 | + if (q > 0) { 83 | + builder.append(StringUtils.repeatString(" ", q)); 84 | + } 85 | + q = 0; 86 | + } else if (ch > 255) { 87 | + q++; 88 | + } 89 | + builder.append(ch); 90 | + } 91 | + 92 | + row = builder.toString(); 93 | + 94 | if(row.length() > maxLength) maxLength = row.length(); 95 | rows.set(index, new StringBuilder(row)); 96 | index++; 97 | Index: src/org/stathissideris/ascii2image/core/HTMLConverter.java 98 | =================================================================== 99 | --- src/org/stathissideris/ascii2image/core/HTMLConverter.java (revision 80) 100 | +++ src/org/stathissideris/ascii2image/core/HTMLConverter.java (working copy) 101 | @@ -139,7 +139,7 @@ 102 | } 103 | 104 | if(diagramList.isEmpty()){ 105 | - System.out.println("\nHTML document does not contain any " + + System.out.println("\nHTML document does not contain any " + 106 | "
 tags with their class attribute set to \""+TAG_CLASS+"\". Nothing to do.");
107 |  			
108 |  			//TODO: should return the method with appropriate exit code instead
109 | @@ -163,7 +163,9 @@
110 |  		
111 |  		System.out.println("Generating diagrams... ");
112 |  		
113 | -		File imageDir = new File(new File(targetFilename).getParent() + File.separator + imageDirName);
114 | +        String imagesParent = new File(targetFilename).getAbsoluteFile().getParent();
115 | +		File imageDir = new File(imagesParent + File.separator + imageDirName);
116 | +
117 |  		if(!imageDir.exists()){
118 |  			if(!imageDir.mkdir()){
119 |  				System.err.println("Could not create directory " + imageDirName);
120 | @@ -173,9 +175,9 @@
121 |  		
122 |  		for(String URL : diagramList.keySet()) {
123 |  			String text = (String) diagramList.get(URL);
124 | -			String imageFilename = new File(targetFilename).getParent() + File.separator + URL;
125 | +			String imageFilename = imagesParent + File.separator + URL;
126 |  			if(new File(imageFilename).exists() && !options.processingOptions.overwriteFiles()){
127 | -				System.out.println("Error: Cannot overwrite file "+URL+", file already exists." +
+				System.out.println("Error: Cannot overwrite file "+URL+", file already exists." +
128 |  					" Use the --overwrite option if you would like to allow file overwrite.");
129 |  				continue;
130 |  			}
131 | 


--------------------------------------------------------------------------------
/bin/ditaa0_9_gbk.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/bin/ditaa0_9_gbk.jar


--------------------------------------------------------------------------------
/bin/gitdown:
--------------------------------------------------------------------------------
  1 | #! /usr/bin/perl
  2 | #   gitdoc - a tool for writing github-hosted documents.
  3 | #
  4 | #   See README.txt for details.
  5 | #
  6 | #   Copyright (c) 1996-2011 iMatix Corporation
  7 | #
  8 | #   This is free software; you can redistribute it and/or modify it under the
  9 | #   terms of the GNU General Public License as published by the Free Software
 10 | #   Foundation; either version 3 of the License, or (at your option) any later
 11 | #   version.
 12 | #
 13 | #   This software is distributed in the hope that it will be useful, but
 14 | #   WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABIL-
 15 | #   ITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 16 | #   License for more details.
 17 | #
 18 | #   You should have received a copy of the GNU General Public License along
 19 | #   with this program. If not, see .
 20 | #
 21 | 
 22 | use File::Basename;
 23 | 
 24 | # push gitdown path to include for modules
 25 | my $path = dirname(__FILE__);
 26 | push(@INC, $path);
 27 | 
 28 | #   Uses the Perl SFL modules from htmlpp
 29 | require 'sflcvdp.pl';                   #   SFL date picture formatting
 30 | 
 31 | $version = "2011.03.24";
 32 | 
 33 | #   Parse and validate provided filename
 34 | die "Syntax: gitdoc textfilename\n"
 35 |     unless $#ARGV == 0;
 36 | $input = $ARGV [0];
 37 | $self = $input;
 38 | $self =~ s/\.[^.]*$//;
 39 | $output .= "$self.md";
 40 | die "Input should be a .txt file, not a .md file\n"
 41 |     if $input eq $output;
 42 | 
 43 | #   Read input file into @input array
 44 | die "Can't read $input: $!"
 45 |     unless open (INPUT, $input);
 46 | while () {
 47 |     chop;
 48 |     push @input, $_;
 49 | }
 50 | close (INPUT);
 51 | #&insert_anchors;
 52 | 
 53 | die "Can't create $output: $!"
 54 |     unless open (OUTPUT, ">$output");
 55 | 
 56 | die "Can't create images.html: $!"
 57 |     unless open (IMAGES, ">images.html");
 58 | writeln_images ("");
 59 | 
 60 | # set defaults for symbols
 61 | $symbols {"INPUT"}  = $input;
 62 | $symbols {"SELF"}   = $self;
 63 | $symbols {"OUTPUT"} = $output;
 64 | $symbols {"PREBRANCH"} = "raw";
 65 | $symbols {"BRANCH"} = "master";
 66 | 
 67 | $imgpath = "";
 68 | 
 69 | $line = 0;
 70 | while ($line < @input) {
 71 |     $_ = $input [$line++];
 72 |     if (/^\./) {
 73 |         #   Process directive
 74 |         if (/^\.set\s+(\w+)=(.*)\s*/) {
 75 |             $symbols {$1} = $2;
 76 | 
 77 |             # probably quicker to rebuild image path on each symbol definition
 78 |             # than to check and see if it's changed
 79 |             $imgpath = $symbols{GIT}.'/';
 80 |             # allow double slash after : for url
 81 |             $imgpath =~ s|(? $tocl) {
110 |                         writeln (" $3");
111 |                     }
112 |                     else {
113 |                         writeln ("\n**$3**");
114 |                     }
115 |                 }
116 |             }
117 |         }
118 |         elsif (/^\.sub\s+([^=]+)=(.*)\s*/) {
119 |             $subsset {$1} = $2;
120 |         }
121 |         elsif (/^\.\-/) {
122 |             #   Comment, ignore
123 |         }
124 |         elsif (/^\.pull (.*)(@[a-zA-Z0-9]+)(,(.*)\s*)?/) {
125 |             $source = $1;
126 |             $tag = $2;
127 |             $opts = $4;
128 |             die "Can't read $source: $!"
129 |                 unless open (SOURCE, $source);
130 |             while () {
131 |                 if (/$tag/) {
132 |                     while () {
133 |                         last if /@[a-zA-Z0-9]+/;
134 |                         chop;
135 |                         $_ = "    $_" if ($opts eq "code");
136 |                         s/^    // if ($opts eq "left");
137 |                         writeln ($_);
138 |                     }
139 |                 }
140 |             }
141 |             close (SOURCE);
142 |         }
143 |         elsif (/^\.end/) {
144 |             writeln ("(More coming soon...)");
145 |             $EOD = 1;       #   Stop output here
146 |         }
147 |         else {
148 |             print "Illegal directive $_ at line $line.\n";
149 |             writeln ($_);
150 |         }
151 |     }
152 | #    elsif ($_ eq "[diagram]") {
153 |     elsif ($_ eq "```textdiagram") {
154 |         #   Shunt diagram into temporary HTML file for Ditaa
155 |         $diagram = $diagram + 1;
156 |         $symbols {"DIAGRAM"} = $diagram;
157 |         writeln_images ("
");
158 |         while ($line < @input) {
159 |             $_ = $input [$line++];
160 |             #last if /\[\/diagram\]/;
161 |             last if /```/;
162 |             s/#/$diagram/;
163 |             writeln_images ($_);
164 |         }
165 |         writeln_images ("
"); 166 | # writeln ("![$diagram](images/$self\_$diagram.png)"); 167 | writeln ("![$diagram]($imgpath$self\_$diagram.png)"); 168 | # writeln ("
"); 169 | # writeln ("\"$diagram\""); 170 | # writeln ("
"); 171 | } 172 | else { 173 | # Normal text 174 | writeln ($_); 175 | } 176 | } 177 | writeln_images (""); 178 | close (IMAGES); 179 | close (OUTPUT); 180 | 181 | system ("rm -f images/$self\_*"); 182 | if ($diagram) { 183 | system ("java -jar $path/ditaa0_9_gbk.jar images.html -e utf-8 -o -h -E output.html"); 184 | # Need to trim twice for reasons I don't care to explore 185 | system ("mogrify -trim images/$self\_*.png"); 186 | system ("mogrify -trim images/$self\_*.png"); 187 | } 188 | system ("rm -f output.html images.html"); 189 | exit (0); 190 | 191 | 192 | # Writes $_ to OUTPUT after expanding all symbols 193 | 194 | sub writeln { 195 | local ($_) = @_; 196 | # Don't expand symbols in code blocks 197 | $_ = expand_symbols ($_) unless /^ /; 198 | foreach $token (keys %subsset) { 199 | s/$token/$subsset{$token}/g; 200 | } 201 | print OUTPUT "$_\n" unless $EOD; 202 | } 203 | 204 | sub writeln_images { 205 | local ($_) = @_; 206 | $_ = expand_symbols ($_); 207 | foreach $token (keys %subsset) { 208 | s/$token/$subsset{$token}/g; 209 | } 210 | print IMAGES "$_\n" unless $EOD; 211 | } 212 | 213 | 214 | # Insert anchors into text before each header 215 | 216 | sub insert_anchors { 217 | $prev = ""; 218 | for ($line = 0; $line < @input; $line++) { 219 | $prev = $_; 220 | $_ = $input [$line]; 221 | 222 | if (/^===/) { 223 | splice @input, $line - 1, 0, ""; 224 | $line++; 225 | } 226 | elsif (/^---/) { 227 | splice @input, $line - 1, 0, ""; 228 | $line++; 229 | } 230 | elsif (/^# /) { 231 | splice @input, $line, 0, ""; 232 | $line++; 233 | } 234 | elsif (/^## /) { 235 | splice @input, $line, 0, ""; 236 | $line++; 237 | } 238 | elsif (/^### /) { 239 | splice @input, $line, 0, ""; 240 | $line++; 241 | } 242 | elsif (/^#### /) { 243 | splice @input, $line, 0, ""; 244 | $line++; 245 | } 246 | } 247 | } 248 | 249 | 250 | #-- Symbol expansion code, taken from htmlpp 251 | 252 | # Recursively expand symbols like this (and in this order): 253 | # 254 | # $(xxx) - value of variable 255 | # $(xxx?zzz) - value of variable, or zzz if undefined 256 | # %(text?zzz) - value of environment variable, or zzz if undef 257 | # &abc(text) - intrinsic function with arguments 258 | # 259 | sub expand_symbols { 260 | local ($_) = @_; 261 | local ($before, 262 | $match, 263 | $after, 264 | $expr); 265 | 266 | return unless ($_); # Quit if input string is empty 267 | 268 | for (;;) { 269 | # Force expansion from end of string first, so things like 270 | # $(xxx?$(yyy)) work properly. 271 | if (/[\$%]\(/ || /\&([a-z_]+)\s*\(/i) { 272 | $before = $`; 273 | $match = $&; 274 | $after = expand_symbols ($'); 275 | $_ = $before.$match.$after; 276 | } 277 | # $(xxx) 278 | if (/\$\(([A-Za-z0-9-_\.]+)\)/) { 279 | $_ = $`.&valueof ($1).$'; 280 | } 281 | # $(xxx?zzz) 282 | elsif (/\$\(([A-Za-z0-9-_\.]+)\?([^)\$]*)\)/) { 283 | $_ = $`.&valueof ($1, $2).$'; 284 | } 285 | # %(text) 286 | elsif (/\%\(([^\)]+)\)/) { 287 | $_ = $`.$ENV {$1}.$'; 288 | } 289 | # %(text?zzz) 290 | elsif (/\%\(([^\)]+)\?([^)\$]*)\)/) { 291 | $_ = $`.($ENV {$1}? $ENV {$1}: $2).$'; 292 | } 293 | # &abc(text) 294 | elsif (/\&([a-z_]+)\s*\(([^\)]*)\)/i) { 295 | $funct = $1; 296 | $args = $2; 297 | $before = $`; 298 | $after = $'; 299 | $args =~ s/\\/\\\\/g; 300 | $_ = eval ("&intrinsic_$funct ($args)"); 301 | $_ = $before.$_.$after; 302 | if ($@) { # Syntax error in Perl statement? 303 | &error ("$function is not a valid intrinsic function") 304 | unless $nofunc_mode; 305 | last; 306 | } 307 | } 308 | else { 309 | last; 310 | } 311 | } 312 | return $_; 313 | } 314 | 315 | 316 | # Subroutine returns the value of the specified symbol; it issues a 317 | # warning message and returns 'UNDEF' if the symbol is not defined 318 | # and the default value is empty. 319 | # 320 | sub valueof { 321 | local ($symbol, $default) = @_; # Argument is symbol name 322 | local ($return); # Returned value 323 | local ($langed_symbol); # Language-dependent symbol 324 | 325 | if (defined ($symbols {$symbol})) { 326 | $return = $symbols {$symbol}; 327 | return $return; 328 | } 329 | elsif (defined ($default)) { 330 | return ($default); 331 | } 332 | &error ("$_"); 333 | &error ("($.) undefined symbol \"$symbol\""); 334 | $default_warning == 1 || do { 335 | &error ("I: Use \$($symbol?default) for default values."); 336 | $default_warning = 1; 337 | }; 338 | $symbols {$symbol} = "UNDEF"; 339 | return $symbols {$symbol}; 340 | } 341 | 342 | 343 | # INTRINSIC FUNCTIONS 344 | # 345 | # time() - Format current time as hh:mm:ss 346 | # date() - Return current date value 347 | # date("picture") - Format current date using picture 348 | # date("picture", date, lc) - Format specified date using picture & language 349 | # week_day([date]) - Get day of week, 0=Sunday to 6=Saturday 350 | # year_week([date]) - Get week of year, 1 is first full week 351 | # julian_date([date]) - Get Julian date for date 352 | # lillian_date([date]) - Get Lillian date for date 353 | # date_to_days(date) - Convert yyyymmdd to Lillian date 354 | # days_to_date(days) - Convert Lillian date to yyyymmdd 355 | # future_date(days[,date]) - Calculate a future date 356 | # past_date(days[,date]) - Calculate a past date 357 | # date_diff(date1[,date2]) - Calculate date1 - date2 358 | # image_height("image.ext") - Get image height (GIF, JPEG) 359 | # image_width("image.ext") - Get image width (GIF, JPEG) 360 | # file_size("filename",arg) - Get size of file: optional arg K or M 361 | # file_date("filename") - Get date of file 362 | # file_time("filename") - Get time of file as hh:mm:ss 363 | # normalise("filename") - Normalise filename to UNIX format 364 | # system("command") - Call a system utility 365 | # lower("string") - Convert string to lower case 366 | # upper("string") - Convert string to upper case 367 | # 368 | 369 | sub intrinsic_date { 370 | local ($picture, $value, $language) = @_; 371 | $value = &date_now unless $value; 372 | $language = $symbols{LANG} unless $language; 373 | if ($picture) { 374 | return (&conv_date_pict ($value, $picture, $language)); 375 | } 376 | else { 377 | return ($value); 378 | } 379 | } 380 | 381 | sub intrinsic_time { 382 | local ($sec, $min, $hour, $day, $month, $year) = localtime; 383 | return (sprintf ("%2d:%02d:%02d", $hour, $min, $sec)); 384 | } 385 | 386 | sub intrinsic_week_day { 387 | return (&day_of_week ($_ [0]? $_ [0]: &date_now)); 388 | } 389 | 390 | sub intrinsic_year_week { 391 | return (&week_of_year ($_ [0]? $_ [0]: &date_now)); 392 | } 393 | 394 | sub intrinsic_julian_date { 395 | return (&julian_date ($_ [0]? $_ [0]: &date_now)); 396 | } 397 | 398 | sub intrinsic_lillian_date { 399 | return (&date_to_days ($_ [0]? $_ [0]: &date_now)); 400 | } 401 | 402 | sub intrinsic_date_to_days { 403 | return (&date_to_days ($_ [0])); 404 | } 405 | 406 | sub intrinsic_days_to_date { 407 | return (&days_to_date ($_ [0])); 408 | } 409 | 410 | sub intrinsic_future_date { 411 | local ($date) = &future_date ($_ [1], 0, $_ [0], 0); 412 | return ($date); 413 | } 414 | 415 | sub intrinsic_past_date { 416 | local ($date) = &past_date ($_ [1], 0, $_ [0], 0); 417 | return ($date); 418 | } 419 | 420 | sub intrinsic_date_diff { 421 | local ($date1, $date2) = @_; 422 | $date1 = &date_now unless $date1; 423 | $date2 = &date_now unless $date2; 424 | local ($days) = &date_diff ($date1, 0, $date2, 0); 425 | return ($days); 426 | } 427 | 428 | sub intrinsic_image_height { 429 | local ($filename) = @_; 430 | if (! -e $filename) { 431 | &error ("($.) file not found: \"$filename\""); 432 | } 433 | else { 434 | return (&image_height ($filename)); 435 | } 436 | } 437 | 438 | sub intrinsic_image_width { 439 | local ($filename) = @_; 440 | if (! -e $filename) { 441 | &error ("($.) file not found: \"$filename\""); 442 | } 443 | else { 444 | return (&image_width ($filename)); 445 | } 446 | } 447 | 448 | sub intrinsic_file_size { 449 | local ($filename, $arg) = @_; 450 | local ($size) = (stat ($filename)) [7]; 451 | 452 | if (! -e $filename) { 453 | &error ("($.) file not found: \"$filename\""); 454 | } 455 | elsif ($arg eq "K") { 456 | $size /= 1024; 457 | } 458 | elsif ($arg eq "M") { 459 | $size /= 1048576; 460 | } 461 | return (int ($size)); 462 | } 463 | 464 | sub intrinsic_file_date { 465 | local ($filename) = @_; 466 | if (! -e $filename) { 467 | &error ("($.) file not found: \"$filename\""); 468 | } 469 | else { 470 | local ($mtime) = (stat ($filename)) [9]; 471 | local ($sec,$min,$hour,$mday,$mon,$year) = localtime ($mtime); 472 | return (($year + 1900) * 10000 + ($mon + 1) * 100 + $mday); 473 | } 474 | } 475 | 476 | sub intrinsic_file_time { 477 | local ($filename) = @_; 478 | if (! -e $filename) { 479 | &error ("($.) file not found: \"$filename\""); 480 | } 481 | else { 482 | local ($mtime) = (stat ($filename)) [9]; 483 | local ($sec,$min,$hour,$mday,$mon,$year) = localtime ($mtime); 484 | return (sprintf ("%2d:%02d:%02d", $hour, $min, $sec)); 485 | } 486 | } 487 | 488 | sub intrinsic_normalise { 489 | local ($_) = @_; # Get filename argument 490 | s/\\/\//g; # Replace DOS-style \ by / 491 | s/\s/_/g; # Replace white space by _ 492 | return ($_); 493 | } 494 | 495 | sub intrinsic_system { 496 | local ($_) = `@_`; 497 | 498 | # Return all but the last character, which should be a newline 499 | chop; 500 | return ($_); 501 | } 502 | 503 | sub intrinsic_lower { 504 | local ($_) = @_; # Get filename argument 505 | tr/A-Z/a-z/; 506 | return ($_); 507 | } 508 | 509 | sub intrinsic_upper { 510 | local ($_) = @_; # Get filename argument 511 | tr/a-z/A-Z/; 512 | return ($_); 513 | } 514 | 515 | sub error { 516 | ($_) = @_; # Get argument 517 | print STDERR "E: $_\n"; 518 | $have_errors = 1; # We have 1 or more errors 519 | } 520 | -------------------------------------------------------------------------------- /bin/sflcvdp.pl: -------------------------------------------------------------------------------- 1 | # 2 | # sflcvdp.pl - SFL convert date to pictures 3 | # 4 | # Copyright (c) 1991-2009 iMatix Corporation 5 | # 6 | # ------------------ GPL Licensed Source Code ------------------ 7 | # iMatix makes this software available under the GNU General 8 | # Public License (GPL) license for open source projects. For 9 | # details of the GPL license please see www.gnu.org or read the 10 | # file license.gpl provided in this package. 11 | # 12 | # This program is free software; you can redistribute it and/or 13 | # modify it under the terms of the GNU General Public License as 14 | # published by the Free Software Foundation; either version 2 of 15 | # the License, or (at your option) any later version. 16 | # 17 | # This program is distributed in the hope that it will be useful, 18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | # GNU General Public License for more details. 21 | # 22 | # You should have received a copy of the GNU General Public 23 | # License along with this program in the file 'license.gpl'; if 24 | # not, write to the Free Software Foundation, Inc., 59 Temple 25 | # Place - Suite 330, Boston, MA 02111-1307, USA. 26 | # 27 | # You can also license this software under iMatix's General Terms 28 | # of Business (GTB) for commercial projects. If you have not 29 | # explicitly licensed this software under the iMatix GTB you may 30 | # only use it under the terms of the GNU General Public License. 31 | # 32 | # For more information, send an email to info@imatix.com. 33 | # -------------------------------------------------------------- 34 | # 35 | 36 | package sflcvdp; 37 | require 'sfldate.pl'; 38 | 39 | CONFIG: { 40 | @month_name_dk = ("januar", "februar", "marts", "april", 41 | "maj", "juni", "juli", "august", "september", 42 | "oktober", "november", "december"); 43 | @day_name_dk = ("søndag", "mandag", "tirsdag", "onsdag", 44 | "torsdag", "fredag", "lørdag"); 45 | @month_name_en = ("January", "February", "March", "April", 46 | "May", "June", "July", "August", "September", 47 | "October", "November", "December"); 48 | @day_name_en = ("Sunday", "Monday", "Tuesday", "Wednesday", 49 | "Thursday", "Friday", "Saturday"); 50 | @month_name_es = ("Enero", "Febrero", "Marzo", "Abril", 51 | "Mayo", "Junio", "Julio", "Agosto", "Septiembre", 52 | "Octubre", "Noviembre", "Diciembre"); 53 | @day_name_es = ("Domingo", "Lunes", "Martes", "Miércoles", 54 | "Jueves", "Viernes", "Sábado"); 55 | @month_name_fr = ("Dimanche", "Lundi", "Mardi", "Mercredi", 56 | "Jeudi", "Vendredi", "Samedi"); 57 | @day_name_fr = ("Janvier", "Février", "Mars", "Avril", 58 | "Mai", "Juin", "Juillet", "Aoüt", "Septembre", 59 | "Octobre", "Novembre", "Décembre"); 60 | } 61 | 62 | # $result = &conv_date_pict ($date, $picture); 63 | # 64 | # The picture is composed of any combination of these formats: 65 | # 66 | # cc century 2 digits, 01-99 67 | # y day of year, 1-366 68 | # yy year 2 digits, 00-99 69 | # yyyy year 4 digits, 100-9999 70 | # m month, 1-12 71 | # mm month, 01-12 72 | # mmm month, 3 letters 73 | # mmmm month, full name 74 | # MMM month, 3 letters, ucase 75 | # MMMM month, full name, ucase 76 | # d day, 1-31 77 | # dd day, 01-31 78 | # ddd day of week, Sun-Sat 79 | # dddd day of week, Sunday-Saturday 80 | # DDD day of week, SUN-SAT 81 | # DDDD day of week, SUNDAY-SATURDAY 82 | # w day of week, 1-7 (1=Sunday) 83 | # ww week of year, 1-53 84 | # q year quarter, 1-4 85 | # \x literal character x 86 | # other literal character 87 | # 88 | # Returns the formatted result. If you pass only a picture string, uses 89 | # today's date. If you pass a second argument, it should be a date value 90 | # containing the date as 8 digits (yyyymmdd). You can also pass a 6-digit 91 | # value (yymmdd) and this subroutine will assume a suitable century. If 92 | # the supplied date value is zero, returns an empty string. 93 | # The 'm' and 'd' formats output a leading space when used at the start 94 | # of the picture. This is to improve alignment of columns of dates. The 95 | # 'm' and 'd' formats also output a space when the previous character was 96 | # a digit; otherwise the date components stick together and are illegible. 97 | # 98 | # Examples: 99 | # &conv_date_pict (19951202, "mm d, yy") Dec 2, 95 100 | # &conv_date_pict (19951202, "d mmm, yy") 2 Dec, 95 101 | # &conv_date_pict (19951202, "yymd") 9512 2 102 | # &conv_date_pict (951202, "yyyymmdd") 19951202 103 | 104 | sub 'conv_date_pict { 105 | # Get subroutine arguments 106 | local ($date, $picture, $language) = @_; 107 | 108 | # Zero or invalid dates are returned as empty string 109 | if ($date == 0 || !&'valid_date ($date)) { 110 | return (""); 111 | } 112 | if ($language =~ /DK/i) { 113 | @month_name = @month_name_dk; 114 | @day_name = @day_name_dk; 115 | } 116 | elsif ($language =~ /ES/i) { 117 | @month_name = @month_name_es; 118 | @day_name = @day_name_es; 119 | } 120 | elsif ($language =~ /FR/i) { 121 | @month_name = @month_name_fr; 122 | @day_name = @day_name_fr; 123 | } 124 | else { 125 | @month_name = @month_name_en; 126 | @day_name = @day_name_en; 127 | } 128 | $date = &'default_century ($date); 129 | local ($century) = &'get_century ($date); 130 | local ($year) = &'get_year ($date); 131 | local ($month) = &'get_month ($date); 132 | local ($day) = &'get_day ($date); 133 | local ($formatted) = ""; 134 | local ($lastch) = ""; # Last character we output 135 | 136 | while ($picture) { 137 | $element = substr ($picture, 0, 1); 138 | if ($element ne "\\") { 139 | $picture =~ /^($element+)/; 140 | $element = $1; # Get picture element; one or more 141 | $picture = $'; # instances of same character 142 | } 143 | if ($element eq "cc") { # century 2 digits, 01-99 144 | $formatted .= sprintf ("%02d", $century); 145 | } 146 | elsif ($element eq "y") { # day of year, 1-366 147 | $formatted .= &'julian_date ($date); 148 | } 149 | elsif ($element eq "yy") { # year 2 digits, 00-99 150 | $formatted .= sprintf ("%02d", $year); 151 | } 152 | elsif ($element eq "yyyy") { # year 4 digits, 0100-9999 153 | $formatted .= sprintf ("%02d%02d", $century, $year); 154 | } 155 | elsif ($element eq "m") { # month, 1-12 156 | $formatted .= sprintf (($lastch =~ /[0-9]/? "%2d": "%d"), $month); 157 | } 158 | elsif ($element eq "mm") { # month, 01-12 159 | $formatted .= sprintf ("%02d", $month); 160 | } 161 | elsif ($element eq "mmm") { # month, 3 letters 162 | $formatted .= substr ($month_name [$month - 1], 0, 3); 163 | } 164 | elsif ($element eq "mmmm") { # month, full name 165 | $formatted .= $month_name [$month - 1]; 166 | } 167 | elsif ($element eq "MMM") { # month, 3-letters, ucase 168 | local ($name) = substr ($month_name [$month - 1], 0, 3); 169 | $name =~ tr/a-z/A-Z/; 170 | $formatted .= $name; 171 | } 172 | elsif ($element eq "MMMM") { # month, full name, ucase 173 | local ($name) = $month_name [$month - 1]; 174 | $name =~ tr/a-z/A-Z/; 175 | $formatted .= $name; 176 | } 177 | elsif ($element eq "d") { # day, 1-31 178 | $formatted .= sprintf ($lastch =~ /[0-9]/? "%2d": "%d", $day); 179 | } 180 | elsif ($element eq "dd") { # day, 01-31 181 | $formatted .= sprintf ("%02d", $day); 182 | } 183 | elsif ($element eq "ddd") { # day of week, Sun-Sat 184 | $formatted .= substr ($day_name [&'day_of_week ($date)], 0, 3); 185 | } 186 | elsif ($element eq "dddd") { # day of week, Sunday-Saturday 187 | $formatted .= $day_name [&'day_of_week ($date)]; 188 | } 189 | elsif ($element eq "DDD") { # day of week, SUN-SAT 190 | $name = substr ($day_name [&'day_of_week ($date)], 0, 3); 191 | $name =~ tr/a-z/A-Z/; 192 | $formatted .= $name; 193 | } 194 | elsif ($element eq "DDDD") { # day of week, SUNDAY-SATURDAY 195 | $name = $day_name [&'day_of_week ($date)]; 196 | $name =~ tr/a-z/A-Z/; 197 | $formatted .= $name; 198 | } 199 | elsif ($element eq "w") { # day of week, 1-7 (1=Sunday) 200 | $formatted .= &'day_of_week ($date) + 1; 201 | } 202 | elsif ($element eq "ww") { # week of year, 1-53 203 | $formatted .= &'week_of_year ($date); 204 | } 205 | elsif ($element eq "q") { # year quarter, 1-4 206 | $formatted .= &'year_quarter ($date); 207 | } 208 | elsif ($element eq "\\") { # literal character follows 209 | $formatted .= substr ($picture, 1, 1); 210 | $picture = substr ($picture, 2); 211 | } 212 | else { 213 | $formatted .= $element; 214 | } 215 | $lastch = substr ($formatted, -1, 1); 216 | } 217 | return ($formatted); 218 | } 219 | 220 | 1; 221 | 222 | -------------------------------------------------------------------------------- /bin/sfldate.pl: -------------------------------------------------------------------------------- 1 | # 2 | # sfldate.pl - SFL date functions 3 | # 4 | # Copyright (c) 1991-2009 iMatix Corporation 5 | # 6 | # Implements the complete set of macros and functions in the SFL date 7 | # package (sfldate.c). Macros are defined in lower-case: &get_month. 8 | # 9 | # ------------------ GPL Licensed Source Code ------------------ 10 | # iMatix makes this software available under the GNU General 11 | # Public License (GPL) license for open source projects. For 12 | # details of the GPL license please see www.gnu.org or read the 13 | # file license.gpl provided in this package. 14 | # 15 | # This program is free software; you can redistribute it and/or 16 | # modify it under the terms of the GNU General Public License as 17 | # published by the Free Software Foundation; either version 2 of 18 | # the License, or (at your option) any later version. 19 | # 20 | # This program is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | # GNU General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU General Public 26 | # License along with this program in the file 'license.gpl'; if 27 | # not, write to the Free Software Foundation, Inc., 59 Temple 28 | # Place - Suite 330, Boston, MA 02111-1307, USA. 29 | # 30 | # You can also license this software under iMatix's General Terms 31 | # of Business (GTB) for commercial projects. If you have not 32 | # explicitly licensed this software under the iMatix GTB you may 33 | # only use it under the terms of the GNU General Public License. 34 | # 35 | # For more information, send an email to info@imatix.com. 36 | # -------------------------------------------------------------- 37 | 38 | package sfldate; 39 | 40 | CONFIG: { 41 | # Julian date calculation: days before 1st of each month in year 42 | @julian_base = ( 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334); 43 | 44 | # Number of days in each month, in a leap year 45 | @month_days = ( 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ); 46 | 47 | # The timezone calculation was plucked from the timelocal.pl module. 48 | local (@epoch) = localtime (0); 49 | $'daylight = (localtime) [8]; # Is daylight savings time? 50 | $'timezone = $epoch [2] * 60 * 60 + $epoch [1] * 60; 51 | if ($'timezone > 0) { # Seconds west of GMT 52 | $'timezone = 24 * 60 * 60 - $'timezone; 53 | $'timezone -= 24 * 60 * 60 54 | if $epoch [5] == 70; # Account for the date line 55 | } 56 | # Interval values, specified in centiseconds 57 | $'INTERVAL_CENTI = 1; 58 | $'INTERVAL_SEC = 100; 59 | $'INTERVAL_MIN = 6000; 60 | $'INTERVAL_HOUR = 360000; 61 | $'INTERVAL_DAY = 8640000; 62 | } 63 | 64 | # 65 | # Implement the date/time access macros from sfldate.h 66 | 67 | sub 'get_century { 68 | local ($date) = @_; 69 | return (int ($date / 1000000)); 70 | } 71 | 72 | sub 'get_ccyear { 73 | local ($date) = @_; 74 | return (int ($date / 10000)); 75 | } 76 | 77 | sub 'get_year { 78 | local ($date) = @_; 79 | return (int (($date % 1000000) / 10000)); 80 | } 81 | 82 | sub 'get_month { 83 | local ($date) = @_; 84 | return (int (($date % 10000) / 100)); 85 | } 86 | 87 | sub 'get_day { 88 | local ($date) = @_; 89 | return ($date % 100); 90 | } 91 | 92 | sub 'get_hour { 93 | local ($time) = @_; 94 | return (int ($time / 1000000)); 95 | } 96 | 97 | sub 'get_minute { 98 | local ($time) = @_; 99 | return (int (($time % 1000000) / 10000)); 100 | } 101 | 102 | sub 'get_second { 103 | local ($time) = @_; 104 | return (int (($time % 10000) / 100)); 105 | } 106 | 107 | sub 'get_centi { 108 | local ($time) = @_; 109 | return ($time % 100); 110 | } 111 | 112 | sub 'make_date { 113 | local ($century, $year, $month, $day) = @_; 114 | return ($century * 1000000 + $year * 10000 + $month * 100 + $day); 115 | } 116 | 117 | sub 'make_time { 118 | local ($hour, $minute, $second, $centi) = @_; 119 | return ($hour * 1000000 + $minute * 10000 + $second * 100 + $centi); 120 | } 121 | 122 | sub 'timeeq { 123 | local ($date1, $time1, $date2, $time2) = @_; 124 | return ($date1 == $date2 && $time1 == $time2); 125 | } 126 | 127 | sub 'timeneq { 128 | local ($date1,$time1,$date2,$time2) = @_; 129 | return ($date1 != $date2 || $time1 != $time2); 130 | } 131 | 132 | sub 'timelt { 133 | local ($date1,$time1,$date2,$time2) = @_; 134 | return ($date1 < $date2 || ($date1 == $date2 && $time1 < $time2)); 135 | } 136 | 137 | sub 'timele { 138 | local ($date1,$time1,$date2,$time2) = @_; 139 | return ($date1 < $date2 || ($date1 == $date2 && $time1 <= $time2)); 140 | } 141 | 142 | sub 'timegt { 143 | local ($date1,$time1,$date2,$time2) = @_; 144 | return ($date1 > $date2 || ($date1 == $date2 && $time1 > $time2)); 145 | } 146 | 147 | sub 'timege { 148 | local ($date1,$time1,$date2,$time2) = @_; 149 | return ($date1 > $date2 || ($date1 == $date2 && $time1 >= $time2)); 150 | } 151 | 152 | 153 | # ------------------------------------------------------------------------- 154 | # &date_now 155 | # 156 | # Synopsis: Returns the current date as a long value (CCYYMMDD). Since 157 | # most system clocks do not return a century, this function assumes that 158 | # all years 80 and above are in the 20th century, and all years 00 to 79 159 | # are in the 21st century. For best results, consume before 1 Jan 2080. 160 | # 161 | # Selftest: 162 | # &date_now > 19970524 163 | # &date_now == &'timer_to_date (date_to_timer (&date_now, &time_now)) 164 | # ------------------------------------------------------------------------- 165 | 166 | sub 'date_now { 167 | local ($day, $month, $year) = (localtime) [3..5]; 168 | return (&'make_date (0, $year + 1900, $month + 1, $day)); 169 | } 170 | 171 | 172 | # ------------------------------------------------------------------------- 173 | # &time_now 174 | # 175 | # Synopsis: Returns the current time as a long value (HHMMSSCC). If the 176 | # system clock does not return centiseconds, these are set to zero. 177 | # 178 | # Selftest: 179 | # &time_now == &'timer_to_time (date_to_timer (&date_now, &time_now)) 180 | # ------------------------------------------------------------------------- 181 | 182 | sub 'time_now { 183 | local ($sec, $min, $hour) = (localtime) [0..2]; 184 | return (&'make_time ($hour, $min, $sec, 0)); 185 | } 186 | 187 | 188 | # ------------------------------------------------------------------------- 189 | # &leap_year (year_value) 190 | # 191 | # Synopsis: Returns 1 if the year is a leap year. You must supply a 192 | # 4-digit value for the year: 90 is taken to mean 90 ad. Handles leap 193 | # centuries correctly. 194 | # 195 | # Selftest: 196 | # &leap_year (1984) == 1 197 | # &leap_year (1985) == 0 198 | # &leap_year (1900) == 0 199 | # &leap_year (2000) == 1 200 | # ------------------------------------------------------------------------- 201 | 202 | sub 'leap_year { 203 | local ($year) = @_; # Get subroutine arguments 204 | 205 | return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0); 206 | } 207 | 208 | 209 | # ------------------------------------------------------------------------- 210 | # &julian_date (date_value) 211 | # 212 | # Synopsis: Returns the number of days since 31 December last year. The 213 | # Julian date of 1 January is 1. 214 | # 215 | # Selftest: 216 | # &julian_date (19970101) == 1 217 | # &julian_date (19970102) == 2 218 | # &julian_date (19970201) == 32 219 | # &julian_date (19971231) == 365 220 | # ------------------------------------------------------------------------- 221 | 222 | sub 'julian_date { 223 | local ($date) = @_; # Get subroutine arguments 224 | 225 | local ($julian); 226 | $julian = $julian_base [&'get_month ($date) - 1] 227 | + &'get_day ($date); 228 | if (&'get_month ($date) > 2 229 | && &'leap_year (&'get_year ($date))) { 230 | $julian++; 231 | } 232 | return ($julian); 233 | } 234 | 235 | 236 | # ------------------------------------------------------------------------- 237 | # &day_of_week (date_value) 238 | # 239 | # Synopsis: Returns the day of the week where 0 is Sunday, 1 is Monday, 240 | # ... 6 is Saturday. Uses Zeller's Congurence algorithm. 241 | # 242 | # Selftest: 243 | # &day_of_week (19961203) == 2 244 | # &day_of_week (19970525) == 0 245 | # -------------------------------------------------------------------------- 246 | 247 | sub 'day_of_week { 248 | local ($date) = @_; # Get subroutine arguments 249 | 250 | local ($year) = &'get_ccyear ($date); 251 | local ($month) = &'get_month ($date); 252 | local ($day) = &'get_day ($date); 253 | if ($month > 2) { 254 | $month -= 2; 255 | } 256 | else { 257 | $month += 10; 258 | $year--; 259 | } 260 | $day = (int ((13 * $month - 1) / 5) + $day + ($year % 100) + 261 | int (($year % 100) / 4) + int (int ($year / 100) / 4) - 2 * 262 | int ($year / 100) + 77); 263 | 264 | return ($day - 7 * int ($day / 7)); 265 | } 266 | 267 | 268 | # ------------------------------------------------------------------------- 269 | # &week_of_year (date_value) 270 | # 271 | # Synopsis: Returns the week of the year, where 1 is the first full week. 272 | # Week 0 may or may not exist in any year. Uses a Lillian date algorithm 273 | # to calculate the week of the year. The week starts on Sunday. 274 | # 275 | # Selftest: 276 | # &week_of_year (19970524) == 20 277 | # &week_of_year (19970526) == 21 278 | # -------------------------------------------------------------------------- 279 | 280 | sub 'week_of_year { 281 | local ($date) = @_; # Get subroutine arguments 282 | 283 | local ($year) = &'get_ccyear ($date) - 1501; 284 | local ($day) = $year * 365 + int ($year / 4) - 29872 + 1 285 | - int ($year / 100) + int (($year - 300) / 400); 286 | 287 | return (int ((&'julian_date ($date) + int (($day + 4) % 7)) / 7)); 288 | } 289 | 290 | 291 | # ------------------------------------------------------------------------- 292 | # &year_quarter (date_value) 293 | # 294 | # Synopsis: Returns the year quarter, 1 to 4, depending on the month 295 | # specified. 296 | # 297 | # Selftest: 298 | # &year_quarter (19970331) == 1 299 | # &year_quarter (19970401) == 2 300 | # &year_quarter (19971231) == 4 301 | # -------------------------------------------------------------------------- 302 | 303 | sub 'year_quarter { 304 | local ($date) = @_; # Get subroutine arguments 305 | return (int ((&'get_month ($date) - 1) / 3 + 1)); 306 | } 307 | 308 | 309 | # ------------------------------------------------------------------------- 310 | # &default_century (date_value) 311 | # 312 | # Synopsis: Supplies a default century for the year if necessary. If 313 | # the year is 51 to 99, the century is set to 19. If the year is 0 to 314 | # 50, the century is set to 20. Returns the adjusted date. 315 | # 316 | # Selftest: 317 | # &default_century (19970525) == 19970525 318 | # &default_century (20070525) == 20070525 319 | # &default_century (970525) == 19970525 320 | # &default_century ( 70525) == 20070525 321 | # -------------------------------------------------------------------------- 322 | 323 | sub 'default_century { 324 | local ($date) = @_; # Get subroutine arguments 325 | 326 | if (&'get_century ($date) == 0) { 327 | $date += &'get_year ($date) > 50? 19000000: 20000000; 328 | } 329 | return ($date); 330 | } 331 | 332 | 333 | # ------------------------------------------------------------------------- 334 | # &pack_date (date_value) 335 | # 336 | # Synopsis: Packs the date into a single unsigned short word. Use this 337 | # function to store dates when memory space is at a premium. The packed 338 | # date can be used correctly in comparisons. Returns the packed date. 339 | # The date must be later than 31 December 1979. 340 | # 341 | # Selftest: 342 | # &pack_date (19800101) == 33 343 | # &pack_date (19970525) == 8889 344 | # &unpack_date (&pack_date ($date = &date_now ())) == $date 345 | # -------------------------------------------------------------------------- 346 | 347 | sub 'pack_date { 348 | local ($date) = @_; # Get subroutine arguments 349 | 350 | return (((&'get_ccyear ($date) - 1980) << 9) + 351 | (&'get_month ($date) << 5) + 352 | &'get_day ($date)); 353 | } 354 | 355 | 356 | # ------------------------------------------------------------------------- 357 | # &pack_time (time_value) 358 | # 359 | # Synopsis: Packs the time into a single unsigned short word. Use this 360 | # function to store times when memory space is at a premium. The packed 361 | # time can be used correctly in comparisons. Returns the packed time. 362 | # Seconds are stored with 2-second accuracy and centiseconds are lost. 363 | # 364 | # Selftest: 365 | # &pack_time (12590000) == 26464; 366 | # &unpack_time (&pack_time ($time = &time_now ())) == int ($time / 200) * 200 367 | # -------------------------------------------------------------------------- 368 | 369 | 370 | sub 'pack_time { 371 | local ($time) = @_; # Get subroutine arguments 372 | 373 | return ((&'get_hour ($time) << 11) + 374 | (&'get_minute ($time) << 5) + 375 | (&'get_second ($time) >> 1)); 376 | } 377 | 378 | 379 | # ------------------------------------------------------------------------- 380 | # &unpack_date (packed_date) 381 | # 382 | # Synopsis: Converts a packed date back into a long value. 383 | # 384 | # Selftest: 385 | # &unpack_date (33) == 19800101 386 | # &unpack_date (8889) == 19970525 387 | # &unpack_date (&pack_date ($date = &date_now ())) == $date 388 | # ------------------------------------------------------------------------- 389 | 390 | sub 'unpack_date 391 | { 392 | local ($packdate) = @_; # Get subroutine arguments 393 | 394 | local ($year); 395 | $year = (($packdate & 0xfe00) >> 9) + 1980; 396 | 397 | return (&'make_date (0, $year, 398 | ($packdate & 0x01e0) >> 5, 399 | ($packdate & 0x001f))); 400 | } 401 | 402 | 403 | # ------------------------------------------------------------------------- 404 | # &unpack_time (packed_time) 405 | # 406 | # Synopsis: Converts a packed time back into a long value. 407 | # 408 | # Selftest: 409 | # &unpack_time (26464) == 12590000; 410 | # &unpack_time (&pack_time ($time = &time_now ())) == int ($time / 200) * 200 411 | # -------------------------------------------------------------------------- 412 | 413 | sub 'unpack_time { 414 | local ($packtime) = @_; # Get subroutine arguments 415 | 416 | return (&'make_time (($packtime & 0xf800) >> 11, 417 | ($packtime & 0x07e0) >> 5, 418 | ($packtime & 0x001f) << 1, 0)); 419 | } 420 | 421 | 422 | # ------------------------------------------------------------------------- 423 | # &date_to_days (date_value) 424 | # 425 | # Synopsis: Converts the date into a number of days since a distant but 426 | # unspecified epoch. You can use this function to calculate differences 427 | # between dates, and forward dates. Use &days_to_date to calculate the 428 | # reverse function. Author: Robert G. Tantzen, translated from the Algol 429 | # original in Collected Algorithms of the CACM (algorithm 199). Original 430 | # translation into C by Nat Howard, posted to Usenet 5 Jul 1985. Perl'd 431 | # By Pieter.(tm) 432 | # 433 | # Selftest: 434 | # &date_to_days (19970525) == 2450594; 435 | # &days_to_date (&date_to_days ($date = &date_now)) == $date 436 | # -------------------------------------------------------------------------- 437 | 438 | sub 'date_to_days { 439 | local ($date) = @_; # Get subroutine arguments 440 | 441 | local ($year) = &'get_year ($date), 442 | local ($century) = &'get_century ($date), 443 | local ($month) = &'get_month ($date), 444 | local ($day) = &'get_day ($date); 445 | if ($month > 2) { 446 | $month -= 3; 447 | } 448 | else { 449 | $month += 9; 450 | if ($year) { 451 | $year--; 452 | } 453 | else { 454 | $year = 99; 455 | $century--; 456 | } 457 | } 458 | return (int ((146097 * $century) / 4) + 459 | int ((1461 * $year) / 4) + 460 | int ((153 * $month + 2) / 5) + 461 | $day + 1721119); 462 | } 463 | 464 | 465 | # ------------------------------------------------------------------------- 466 | # &days_to_date (number_of_days) 467 | # 468 | # Synopsis: Converts a number of days since some distant but unspecified 469 | # epoch into a date. You can use this function to calculate differences 470 | # between dates, and forward dates. Use &date_to_days to calculate the 471 | # reverse function. Author: Robert G. Tantzen, translated from the Algol 472 | # original in Collected Algorithms of the CACM (algorithm 199). Original 473 | # translation into C by Nat Howard, posted to Usenet 5 Jul 1985. 474 | # 475 | # Selftest: 476 | # &date_to_days (19970525) == 2450594; 477 | # &days_to_date (&date_to_days ($date = &date_now)) == $date 478 | # -------------------------------------------------------------------------- 479 | 480 | sub 'days_to_date { 481 | local ($days) = @_; # Get subroutine arguments 482 | 483 | local ($century, $year, $month, $day); 484 | $days -= 1721119; 485 | $century = int ((4 * $days - 1) / 146097); 486 | $days = 4 * $days - 1 - 146097 * $century; 487 | $day = int ($days / 4); 488 | 489 | $year = int ((4 * $day + 3) / 1461); 490 | $day = 4 * $day + 3 - 1461 * $year; 491 | $day = int (($day + 4) / 4); 492 | 493 | $month = int ((5 * $day - 3) / 153); 494 | $day = 5 * $day - 3 - 153 * $month; 495 | $day = int (($day + 5) / 5); 496 | 497 | if ($month < 10) { 498 | $month += 3; 499 | } 500 | else { 501 | $month -= 9; 502 | $year++; # $year may overflow to 100 503 | } 504 | return (&'make_date ($century, $year, $month, $day)); 505 | } 506 | 507 | 508 | # ---------------------------------------------------------------------[<]- 509 | # $timer = &date_to_timer (date_value, time_value) 510 | # 511 | # Synopsis: Converts the supplied date and time into a timer value, which 512 | # is the number of non-leap seconds since 00:00:00 UTC January 1, 1970. 513 | # 514 | # Selftest: 515 | # &date_now == &timer_to_date (date_to_timer (&date_now, &time_now)) 516 | # ---------------------------------------------------------------------[>]-*/ 517 | 518 | sub 'date_to_timer { 519 | local ($date, $time) = @_; # Get subroutine arguments 520 | 521 | # Get number of days since 1 January, 1970 522 | local ($days) = &'date_to_days ($date) - 2440588; 523 | local ($seconds) = (( $days * 24 524 | + &'get_hour ($time) - $'daylight) * 60 525 | + &'get_minute ($time)) * 60 526 | + &'get_second ($time); 527 | return ($seconds + $'timezone); 528 | } 529 | 530 | 531 | # ---------------------------------------------------------------------[<]- 532 | # &timer_to_date 533 | # 534 | # Synopsis: Converts the supplied timer value into a long date value. 535 | # Dates are stored as long values: CCYYMMDD. If the supplied value is 536 | # zero, returns zero. 537 | # 538 | # Selftest: 539 | # &date_now == &timer_to_date (date_to_timer (&date_now, &time_now)) 540 | # ---------------------------------------------------------------------[>]-*/ 541 | 542 | sub 'timer_to_date { 543 | local ($time_secs) = @_; # Get subroutine arguments 544 | 545 | if ($time_secs == 0) { 546 | return (0); 547 | } 548 | else { 549 | # Convert into a long value CCYYMMDD 550 | local ($day, $month, $year) = (localtime ($time_secs)) [3..5]; 551 | return (&'make_date (0, $year + 1900, $month + 1, $day)); 552 | } 553 | } 554 | 555 | 556 | # ---------------------------------------------------------------------[<]- 557 | # &timer_to_time 558 | # 559 | # Synopsis: Converts the supplied timer value into a long time value. 560 | # Times are stored as long values: HHMMSS00. Since the timer value does 561 | # not hold centiseconds, these are set to zero. If the supplied value 562 | # was zero, returns zero. 563 | # 564 | # Selftest: 565 | # &time_now == &timer_to_time (date_to_timer (&date_now, &time_now)) 566 | # ---------------------------------------------------------------------[>]-*/ 567 | 568 | sub 'timer_to_time { 569 | local ($time_secs) = @_; # Get subroutine arguments 570 | 571 | if ($time_secs == 0) { 572 | return (0); 573 | } 574 | else { 575 | local ($sec, $min, $hour) = (localtime ($time_secs)) [0..2]; 576 | return (&'make_time ($hour, $min, $sec, 0)); 577 | } 578 | } 579 | 580 | 581 | # ---------------------------------------------------------------------[<]- 582 | # &timer_to_gmdate 583 | # 584 | # Synopsis: Converts the supplied timer value into a long date value in 585 | # Greenwich Mean Time (GMT). Dates are stored as long values: CCYYMMDD. 586 | # If the supplied value is zero, returns zero. 587 | # 588 | # Selftest: 589 | # &timer_to_gmdate (100000) == &timer_to_date (100000 + timezone) 590 | # ---------------------------------------------------------------------[>]-*/ 591 | 592 | sub 'timer_to_gmdate { 593 | local ($time_secs) = @_; # Get subroutine arguments 594 | 595 | if ($time_secs == 0) { 596 | return (0); 597 | } 598 | else { 599 | # Convert into a long value CCYYMMDD 600 | local ($day, $month, $year) = (gmtime ($time_secs)) [3..5]; 601 | return (&'make_date (0, $year + 1900, $month + 1, $day)); 602 | } 603 | } 604 | 605 | 606 | # ---------------------------------------------------------------------[<]- 607 | # &timer_to_gmtime 608 | # 609 | # Synopsis: Converts the supplied timer value into a long time value in 610 | # Greenwich Mean Time (GMT). Times are stored as long values: HHMMSS00. 611 | # On most systems the clock does not return centiseconds, so these are 612 | # set to zero. If the supplied value is zero, returns zero. 613 | # 614 | # Selftest: 615 | # &timer_to_gmtime (100000) == &timer_to_time (100000 + $timezone) 616 | # ---------------------------------------------------------------------[>]-*/ 617 | 618 | sub 'timer_to_gmtime { 619 | local ($time_secs) = @_; # Get subroutine arguments 620 | 621 | if ($time_secs == 0) { 622 | return (0); 623 | } 624 | else { 625 | local ($sec, $min, $hour) = (gmtime ($time_secs)) [0..2]; 626 | return (&'make_time ($hour, $min, $sec, 0)); 627 | } 628 | } 629 | 630 | 631 | # ------------------------------------------------------------------------- 632 | # &time_to_csecs (time_value) 633 | # 634 | # Synopsis: Converts a time (HHMMSSCC) into a number of centiseconds. 635 | # 636 | # Selftest: 637 | # &csecs_to_time (&time_to_csecs ($time = &time_now)) == $time 638 | # -------------------------------------------------------------------------- 639 | 640 | sub 'time_to_csecs { 641 | local ($time) = @_; # Get subroutine arguments 642 | 643 | return (&'get_hour ($time) * $'INTERVAL_HOUR 644 | + &'get_minute ($time) * $'INTERVAL_MIN 645 | + &'get_second ($time) * $'INTERVAL_SEC 646 | + &'get_centi ($time)); 647 | } 648 | 649 | 650 | # ------------------------------------------------------------------------- 651 | # &csecs_to_time (centiseconds) 652 | # 653 | # Synopsis: Converts a number of centiseconds (< INTERVAL_DAY) into a 654 | # time value (HHMMSSCC). 655 | # 656 | # Selftest: 657 | # &csecs_to_time (&time_to_csecs ($time = &time_now)) == $time 658 | # -------------------------------------------------------------------------- 659 | 660 | sub 'csecs_to_time { 661 | local ($csecs) = @_; # Get subroutine arguments 662 | 663 | local ($hour, $min, $sec); 664 | $hour = int ($csecs / $'INTERVAL_HOUR); 665 | $csecs = $csecs % $'INTERVAL_HOUR; 666 | $min = int ($csecs / $'INTERVAL_MIN); 667 | $csecs = $csecs % $'INTERVAL_MIN; 668 | $sec = int ($csecs / $'INTERVAL_SEC); 669 | $csecs = $csecs % $'INTERVAL_SEC; 670 | 671 | return (&'make_time ($hour, $min, $sec, $csecs)); 672 | } 673 | 674 | 675 | # ------------------------------------------------------------------------- 676 | # ($date, $time) = &future_date ($date, $time, days, csecs) 677 | # 678 | # Synopsis: Calculates a future date and time from the date and time 679 | # specified, plus an interval specified in days and 1/100th seconds. 680 | # The date can be any date since some distant epoch (around 1600). 681 | # If the date and time arguments are both zero, the current date and 682 | # time are used. 683 | # 684 | # Selftest: 685 | # &future_date (19970525, 12000000, 1, 100) == (19970526, 12000100) 686 | # &future_date (19970531, 23595900, 0, 100) == (19970601, 0) 687 | # -------------------------------------------------------------------------- 688 | 689 | sub 'future_date { 690 | local ($date, $time, $days, $csecs) = @_; 691 | 692 | # Set date and time to NOW if necessary 693 | if ($date == 0 && $time == 0) { 694 | $date = &'date_now; 695 | $time = &'time_now; 696 | } 697 | # Get future date in days and centiseconds 698 | $days = &'date_to_days ($date) + $days; 699 | $csecs = &'time_to_csecs ($time) + $csecs; 700 | 701 | # Normalise overflow in centiseconds 702 | while ($csecs >= $'INTERVAL_DAY) { 703 | $days++; 704 | $csecs -= $'INTERVAL_DAY; 705 | } 706 | # Convert date and time back into organised values 707 | $date = &'days_to_date ($days); 708 | $time = &'csecs_to_time ($csecs); 709 | 710 | return ($date, $time); 711 | } 712 | 713 | 714 | # ------------------------------------------------------------------------- 715 | # ($date, $time) = &past_date ($date, $time, days, csecs) 716 | # 717 | # Synopsis: Calculates a past date and time from the date and time 718 | # specified, minus an interval specified in days and 1/100th seconds. 719 | # The date can be any date since some distant epoch (around 1600). 720 | # If the date and time arguments are both zero, the current date and 721 | # time are used. 722 | # 723 | # Selftest: 724 | # &past_date (19970526, 12000100, 1, 100) == (19970525, 12000000) 725 | # &past_date (19970601, 0, 0, 100) == (19970531, 23595900) 726 | # -------------------------------------------------------------------------- 727 | 728 | sub 'past_date { 729 | local ($date, $time, $days, $csecs) = @_; 730 | 731 | # Set date and time to NOW if necessary 732 | if ($date == 0 && $time == 0) { 733 | $date = &'date_now; 734 | $time = &'time_now; 735 | } 736 | # Get past date in days and centiseconds 737 | $days = &'date_to_days ($date) - $days; 738 | $csecs = &'time_to_csecs ($time) - $csecs; 739 | 740 | # Normalise underflow in centiseconds 741 | while ($csecs < 0) { 742 | $days--; 743 | $csecs += $'INTERVAL_DAY; 744 | } 745 | # Convert date and time back into organised values 746 | $date = &'days_to_date ($days); 747 | $time = &'csecs_to_time ($csecs); 748 | 749 | return ($date, $time); 750 | } 751 | 752 | 753 | # ------------------------------------------------------------------------- 754 | # ($days, $csecs) = &date_diff ($date1, $time1, $date2, $time2) 755 | # 756 | # Synopsis: Calculates the difference between two date/time values, and 757 | # returns the difference as a number of days and a number of centiseconds. 758 | # The date can be any date since some distant epoch (around 1600). The 759 | # calculation is date1:time1 - date2:time2. The returned values may be 760 | # negative. 761 | # 762 | # Selftest: 763 | # &date_diff (19970526, 12000100, 19970525, 12000000) == (1, 100) 764 | # -------------------------------------------------------------------------- 765 | 766 | sub 'date_diff { 767 | local ($date1, $time1, $date2, $time2) = @_; 768 | 769 | local ($days, $csecs); 770 | $days = &'date_to_days ($date1) - &'date_to_days ($date2); 771 | $csecs = &'time_to_csecs ($time1) - &'time_to_csecs ($time2); 772 | return ($days, $csecs); 773 | } 774 | 775 | 776 | # ------------------------------------------------------------------------- 777 | # &valid_date (date_value) 778 | # 779 | # Synopsis: Returns 1 if the date is valid or zero; returns 0 if the date 780 | # is not valid. 781 | # 782 | # Selftest: 783 | # &valid_date (19970526) == 1 784 | # &valid_date (19970532) == 0 785 | # &valid_date (19970229) == 0 786 | # &valid_date (20000229) == 1 787 | # -------------------------------------------------------------------------- 788 | 789 | sub 'valid_date { 790 | local ($date) = @_; # Get subroutine arguments 791 | 792 | local ($month, $day, $feedback); 793 | $month = &'get_month ($date); 794 | $day = &'get_day ($date); 795 | 796 | if ($date == 0) { 797 | $feedback = 1; # Zero date is okay 798 | } 799 | elsif ($month < 1 || $month > 12) { 800 | $feedback = 0; # Month out of range 801 | } 802 | elsif (($day < 1 || $day > $month_days [$month - 1]) 803 | || ($month == 2 && $day == 29 804 | && !&'leap_year (&'get_year ($date)))) { 805 | $feedback = 0; # Day out of range 806 | } 807 | else { 808 | $feedback = 1; # Zero date is okay 809 | } 810 | return ($feedback); 811 | } 812 | 813 | 814 | # ------------------------------------------------------------------------- 815 | # &valid_time (time_value) 816 | # 817 | # Synopsis: Returns TRUE if the time is valid or zero; returns FALSE if 818 | # the time is not valid. 819 | # 820 | # Selftest: 821 | # &valid_time (19596000) == 0 822 | # &valid_time (19595999) == 1 823 | # &valid_time (24000000) == 0 824 | # &valid_time ( 0) == 1 825 | # -------------------------------------------------------------------------- 826 | 827 | sub 'valid_time { 828 | local ($time) = @_; # Get subroutine arguments 829 | 830 | return (&'get_second ($time) < 60 831 | && &'get_minute ($time) < 60 832 | && &'get_hour ($time) < 24); 833 | } 834 | 835 | 836 | # ------------------------------------------------------------------------- 837 | # &date_is_future (date_value, time_value) 838 | # 839 | # Synopsis: Returns TRUE if the specified date and time are in the future. 840 | # Returns FALSE if the date and time are in the past, or the present (which 841 | # will be the past by the time you've read this). Date is specified as a 842 | # YYYYMMDD value; time as HHMMSSCC. 843 | # 844 | # Selftest: 845 | # &date_is_future (&future_date (&date_now, &time_now, 0, 1000)) == 1 846 | # &date_is_future (&future_date (&date_now, &time_now, 0, 0)) == 0 847 | # -------------------------------------------------------------------------- 848 | 849 | sub 'date_is_future { 850 | local ($date, $time) = @_; # Get subroutine arguments 851 | 852 | return ($date > &'date_now 853 | || $date == &'date_now && $time > &'time_now); 854 | } 855 | 856 | 857 | # ------------------------------------------------------------------------- 858 | # &date_is_past (date_value, time_value) 859 | # 860 | # Synopsis: Returns TRUE if the specified date and time are in the past. 861 | # Returns FALSE if the date and time are in the future or present (which 862 | # despite any assertion to the contrary, is not the past. Although that 863 | # may change soon). Date is specified as YYYYMMDD; time as HHMMSSCC. 864 | # 865 | # Selftest: 866 | # &date_is_past (&past_date (&date_now, &time_now, 0, 1000)) == 1 867 | # &date_is_past (&past_date (&date_now, &time_now, 0, 0)) == 0 868 | # -------------------------------------------------------------------------- 869 | 870 | sub 'date_is_past { 871 | local ($date, $time) = @_; # Get subroutine arguments 872 | 873 | return ($date < &'date_now 874 | || $date == &'date_now && $time < &'time_now); 875 | } 876 | -------------------------------------------------------------------------------- /chapter1.md: -------------------------------------------------------------------------------- 1 | 2 | # ZMQ 指南 3 | 4 | **作者: Pieter Hintjens , CEO iMatix Corporation.** 5 | **翻译: 张吉 , 安居客集团 好租网工程师** 6 | 7 | With thanks to Bill Desmarais, Brian Dorsey, CAF, Daniel Lin, Eric Desgranges, Gonzalo Diethelm, Guido Goldstein, Hunter Ford, Kamil Shakirov, Martin Sustrik, Mike Castleman, Naveen Chawla, Nicola Peduzzi, Oliver Smith, Olivier Chamoux, Peter Alexander, Pierre Rouleau, Randy Dryburgh, John Unwin, Alex Thomas, Mihail Minkov, Jeremy Avnet, Michael Compton, Kamil Kisiel, Mark Kharitonov, Guillaume Aubert, Ian Barber, Mike Sheridan, Faruk Akgul, Oleg Sidorov, Lev Givon, Allister MacLeod, Alexander D'Archangel, Andreas Hoelzlwimmer, Han Holl, Robert G. Jakabosky, Felipe Cruz, Marcus McCurdy, Mikhail Kulemin, Dr. Gergő Érdi, Pavel Zhukov, Alexander Else, Giovanni Ruggiero, Rick "Technoweenie", Daniel Lundin, Dave Hoover, Simon Jefford, Benjamin Peterson, Justin Case, Devon Weller, Richard Smith, Alexander Morland, Wadim Grasza, Michael Jakl, and Zed Shaw for their contributions, and to Stathis Sideris for [Ditaa](http://www.ditaa.org). 8 | 9 | Please use the [issue tracker](https://github.com/imatix/zguide/issues) for all comments and errata. This version covers the latest stable release of 0MQ and was published on Mon 10 October, 2011. 10 | 11 | The Guide is mainly [in C](http://zguide.zeromq.org/page:all), but also in [PHP](http://zguide.zeromq.org/php:all) and [Lua](http://zguide.zeromq.org/lua:all). 12 | 13 | --- 14 | 15 | This work is licensed under a [Creative Commons Attribution-ShareAlike 3.0 License](http://creativecommons.org/licenses/by-sa/3.0/). 16 | 17 | ## 第一章 ZeroMQ基础 18 | 19 | ### 拯救世界 20 | 21 | 如何解释ZMQ?有些人会先说一堆ZMQ的好:它是一套用于快速构建的套接字组件;它的信箱系统有超强的路由能力;它太快了!而有些人则喜欢分享他们被ZMQ点悟的时刻,那些被灵感击中的瞬间:所有的事情突然变得简单明了,让人大开眼界。另一些人则会拿ZMQ同其他产品做个比较:它更小,更简单,但却让人觉得如此熟悉。对于我个人而言,我则更倾向于和别人分享ZMQ的诞生史,相信会和各位读者有所共鸣。 22 | 23 | 编程是一门科学,但往往会乔装成一门艺术。我们从不去了解软件最底层的机理,或者说根本没有人在乎这些。软件并不只是算法、数据结构、编程语言、或者抽象云云,这些不过是一些工具而已,被我们创造、使用、最后抛弃。软件真正的本质,其实是人的本质。 24 | 25 | 举例来说,当我们遇到一个高度复杂的问题时,我们会群策群力,分工合作,将问题拆分为若干个部分,一起解决。这里就体现了编程的科学:创建一组小型的构建模块,让人们易于理解和使用,那么大家就会一起用它来解决问题。 26 | 27 | 我们生活在一个普遍联系的世界里,需要现代的编程软件为我们做指引。所以,未来我们所需要的用于处理大规模计算的构建模块,必须是普遍联系的,而且能够并行运作。那时,程序代码不能再只关注自己,它们需要互相交流,变得足够健谈。程序代码需要像人脑一样,数以兆计的神经元高速地传输信号,在一个没有中央控制的环境下,没有单点故障的环境下,解决问题。这一点其实并不意外,因为就当今的网络来讲,每个节点其实就像是连接了一个人脑一样。 28 | 29 | 如果你曾和线程、协议、或网络打过交道,你会觉得我上面的话像是天方夜谭。因为在实际应用过程中,只是连接几个程序或网络就已经非常困难和麻烦了。数以兆计的节点?那真是无法想象的。现今只有资金雄厚的企业才能负担得起这种软件和服务。 30 | 31 | 当今世界的网络结构已经远远超越了我们自身的驾驭能力。二十世纪八十年代的软件危机,弗莱德•布鲁克斯曾说过,这个世上[没有银弹](http://en.wikipedia.org/wiki/No_Silver_Bullet 32 | )。后来,免费和开源解决了这次软件危机,让我们能够高效地分享知识。如今,我们又面临一次新的软件危机,只不过我们谈论得不多。只有那些大型的、富足的企业才有财力建立高度联系的应用程序。那里有云的存在,但它是私有的。我们的数据和知识正在从我们的个人电脑中消失,流入云端,无法获得或与其竞争。是谁坐拥我们的社交网络?这真像一次巨型主机的革命。 33 | 34 | 我们暂且不谈其中的政治因素,光那些就可以另外出本书了。目前的现状是,虽然互联网能够让千万个程序相连,但我们之中的大多数却无法做到这些。这样一来,那些真正有趣的大型问题(如健康、教育、经济、交通等领域),仍然无法解决。我们没有能力将代码连接起来,也就不能像大脑中的神经元一样处理那些大规模的问题。 35 | 36 | 已经有人尝试用各种方法来连接应用程序,如数以千计的IETF规范,每种规范解决一个特定问题。对于开发人员来说,HTTP协议是比较简单和易用的,但这也往往让问题变得更糟,因为它鼓励人们形成一种重服务端、轻客户端的思想。 37 | 38 | 所以迄今为止人们还在使用原始的TCP/UDP协议、私有协议、HTTP协议、网络套接字等形式连接应用程序。这种做法依旧让人痛苦,速度慢又不易扩展,需要集中化管理。而分布式的P2P协议又仅仅适用于娱乐,而非真正的应用。有谁会使用Skype或者Bittorrent来交换数据呢? 39 | 40 | 这就让我们回归到编程科学的问题上来。想要拯救这个世界,我们需要做两件事情:一,如何在任何地点连接任何两个应用程序;二、将这个解决方案用最为简单的方式包装起来,供程序员使用。 41 | 42 | 也许这听起来太简单了,但事实确实如此。 43 | 44 | ### ZMQ简介 45 | 46 | ZMQ(ØMQ、ZeroMQ, 0MQ)看起来像是一套嵌入式的网络链接库,但工作起来更像是一个并发式的框架。它提供的套接字可以在多种协议中传输消息,如线程间、进程间、TCP、广播等。你可以使用套接字构建多对多的连接模式,如扇出、发布-订阅、任务分发、请求-应答等。ZMQ的快速足以胜任集群应用产品。它的异步I/O机制让你能够构建多核应用程序,完成异步消息处理任务。ZMQ有着多语言支持,并能在几乎所有的操作系统上运行。ZMQ是[iMatix][]公司的产品,以LGPL开源协议发布。 47 | 48 | ### 需要具备的知识 49 | 50 | * 使用最新的ZMQ稳定版本; 51 | * 使用Linux系统或其他相似的操作系统; 52 | * 能够阅读C语言代码,这是本指南示例程序的默认语言; 53 | * 当我们书写诸如PUSH或SUBSCRIBE等常量时,你能够找到相应语言的实现,如ZMQ_PUSH、ZMQ_SUBSCRIBE。 54 | 55 | ### 获取示例 56 | 57 | 本指南的所有示例都存放于[github仓库](https://github.com/imatix/zguide)中,最简单的获取方式是运行以下代码: 58 | 59 | ``` 60 | git clone git://github.com/imatix/zguide.git 61 | ``` 62 | 63 | 浏览examples目录,你可以看到多种语言的实现。如果其中缺少了某种你正在使用的语言,我们很希望你可以[提交一份补充](http://zguide.zeromq.org/main:translate)。这也是本指南实用的原因,要感谢所有做出过贡献的人。 64 | 65 | 所有的示例代码都以MIT/X11协议发布,若在源代码中有其他限定的除外。 66 | 67 | ### 提问-回答 68 | 69 | 让我们从简单的代码开始,一段传统的Hello World程序。我们会创建一个客户端和一个服务端,客户端发送Hello给服务端,服务端返回World。下文是C语言编写的服务端,它在5555端口打开一个ZMQ套接字,等待请求,收到后应答World。 70 | 71 | **hwserver.c: Hello World server** 72 | 73 | ```c 74 | // 75 | // Hello World 服务端 76 | // 绑定一个REP套接字至tcp://*:5555 77 | // 从客户端接收Hello,并应答World 78 | // 79 | #include 80 | #include 81 | #include 82 | #include 83 | 84 | int main (void) 85 | { 86 | void *context = zmq_init (1); 87 | 88 | // 与客户端通信的套接字 89 | void *responder = zmq_socket (context, ZMQ_REP); 90 | zmq_bind (responder, "tcp://*:5555"); 91 | 92 | while (1) { 93 | // 等待客户端请求 94 | zmq_msg_t request; 95 | zmq_msg_init (&request); 96 | zmq_recv (responder, &request, 0); 97 | printf ("收到 Hello\n"); 98 | zmq_msg_close (&request); 99 | 100 | // 做些“处理” 101 | sleep (1); 102 | 103 | // 返回应答 104 | zmq_msg_t reply; 105 | zmq_msg_init_size (&reply, 5); 106 | memcpy (zmq_msg_data (&reply), "World", 5); 107 | zmq_send (responder, &reply, 0); 108 | zmq_msg_close (&reply); 109 | } 110 | // 程序不会运行到这里,以下只是演示我们应该如何结束 111 | zmq_close (responder); 112 | zmq_term (context); 113 | return 0; 114 | } 115 | ``` 116 | 117 | ![1](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_1.png) 118 | 119 | 使用REQ-REP套接字发送和接受消息是需要遵循一定规律的。客户端首先使用zmq_send()发送消息,再用zmq_recv()接收,如此循环。如果打乱了这个顺序(如连续发送两次)则会报错。类似地,服务端必须先进行接收,后进行发送。 120 | 121 | ZMQ使用C语言作为它参考手册的语言,本指南也以它作为示例程序的语言。如果你正在阅读本指南的在线版本,你可以看到示例代码的下方有其他语言的实现。如以下是C++语言: 122 | 123 | **hwserver.cpp: Hello World server** 124 | 125 | ```cpp 126 | // 127 | // Hello World 服务端 C++语言版 128 | // 绑定一个REP套接字至tcp://*:5555 129 | // 从客户端接收Hello,并应答World 130 | // 131 | #include 132 | #include 133 | #include 134 | #include 135 | 136 | int main () { 137 | // 准备上下文和套接字 138 | zmq::context_t context (1); 139 | zmq::socket_t socket (context, ZMQ_REP); 140 | socket.bind ("tcp://*:5555"); 141 | 142 | while (true) { 143 | zmq::message_t request; 144 | 145 | // 等待客户端请求 146 | socket.recv (&request); 147 | std::cout << "收到 Hello" << std::endl; 148 | 149 | // 做一些“处理” 150 | sleep (1); 151 | 152 | // 应答World 153 | zmq::message_t reply (5); 154 | memcpy ((void *) reply.data (), "World", 5); 155 | socket.send (reply); 156 | } 157 | return 0; 158 | } 159 | ``` 160 | 161 | 可以看到C语言和C++语言的API代码差不多,而在PHP这样的语言中,代码就会更为简洁: 162 | 163 | **hwserver.php: Hello World server** 164 | 165 | ```php 166 | 172 | */ 173 | 174 | $context = new ZMQContext(1); 175 | 176 | // 与客户端通信的套接字 177 | $responder = new ZMQSocket($context, ZMQ::SOCKET_REP); 178 | $responder->bind("tcp://*:5555"); 179 | 180 | while(true) { 181 | // 等待客户端请求 182 | $request = $responder->recv(); 183 | printf ("Received request: [%s]\n", $request); 184 | 185 | // 做一些“处理” 186 | sleep (1); 187 | 188 | // 应答World 189 | $responder->send("World"); 190 | } 191 | ``` 192 | 193 | 下面是客户端的代码: 194 | 195 | **hwclient: Hello World client in C** 196 | 197 | ```c 198 | // 199 | // Hello World 客户端 200 | // 连接REQ套接字至 tcp://localhost:5555 201 | // 发送Hello给服务端,并接收World 202 | // 203 | #include 204 | #include 205 | #include 206 | #include 207 | 208 | int main (void) 209 | { 210 | void *context = zmq_init (1); 211 | 212 | // 连接至服务端的套接字 213 | printf ("正在连接至hello world服务端...\n"); 214 | void *requester = zmq_socket (context, ZMQ_REQ); 215 | zmq_connect (requester, "tcp://localhost:5555"); 216 | 217 | int request_nbr; 218 | for (request_nbr = 0; request_nbr != 10; request_nbr++) { 219 | zmq_msg_t request; 220 | zmq_msg_init_size (&request, 5); 221 | memcpy (zmq_msg_data (&request), "Hello", 5); 222 | printf ("正在发送 Hello %d...\n", request_nbr); 223 | zmq_send (requester, &request, 0); 224 | zmq_msg_close (&request); 225 | 226 | zmq_msg_t reply; 227 | zmq_msg_init (&reply); 228 | zmq_recv (requester, &reply, 0); 229 | printf ("接收到 World %d\n", request_nbr); 230 | zmq_msg_close (&reply); 231 | } 232 | zmq_close (requester); 233 | zmq_term (context); 234 | return 0; 235 | } 236 | ``` 237 | 238 | 这看起来是否太简单了?ZMQ就是这样一个东西,你往里加点儿料就能制作出一枚无穷能量的原子弹,用它来拯救世界吧! 239 | 240 | ![2](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_2.png) 241 | 242 | 理论上你可以连接千万个客户端到这个服务端上,同时连接都没问题,程序仍会运作得很好。你可以尝试一下先打开客户端,再打开服务端,可以看到程序仍然会正常工作,想想这意味着什么。 243 | 244 | 让我简单介绍一下这两段程序到底做了什么。首先,他们创建了一个ZMQ上下文,然后是一个套接字。不要被这些陌生的名词吓到,后面我们都会讲到。服务端将REP套接字绑定到5555端口上,并开始等待请求,发出应答,如此循环。客户端则是发送请求并等待服务端的应答。 245 | 246 | 这些代码背后其实发生了很多很多事情,但是程序员完全不必理会这些,只要知道这些代码短小精悍,极少出错,耐高压。这种通信模式我们称之为请求-应答模式,是ZMQ最直接的一种应用。你可以拿它和RPC及经典的C/S模型做类比。 247 | 248 | ### 关于字符串 249 | 250 | ZMQ不会关心发送消息的内容,只要知道它所包含的字节数。所以,程序员需要做一些工作,保证对方节点能够正确读取这些消息。如何将一个对象或复杂数据类型转换成ZMQ可以发送的消息,这有类似Protocol Buffers的序列化软件可以做到。但对于字符串,你也是需要有所注意的。 251 | 252 | 在C语言中,字符串都以一个空字符结尾,你可以像这样发送一个完整的字符串: 253 | 254 | ```c 255 | zmq_msg_init_data (&request, "Hello", 6, NULL, NULL); 256 | ``` 257 | 258 | 但是,如果你用其他语言发送这个字符串,很可能不会包含这个空字节,如你使用Python发送: 259 | 260 | ```python 261 | socket.send ("Hello") 262 | ``` 263 | 264 | 实际发送的消息是: 265 | 266 | ![3](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_3.png) 267 | 268 | 如果你从C语言中读取该消息,你会读到一个类似于字符串的内容,甚至它可能就是一个字符串(第六位在内存中正好是一个空字符),但是这并不合适。这样一来,客户端和服务端对字符串的定义就不统一了,你会得到一些奇怪的结果。 269 | 270 | 当你用C语言从ZMQ中获取字符串,你不能够相信该字符串有一个正确的结尾。因此,当你在接受字符串时,应该建立多一个字节的缓冲区,将字符串放进去,并添加结尾。 271 | 272 | 所以,让我们做如下假设:**ZMQ的字符串是有长度的,且传送时不加结束符**。在最简单的情况下,ZMQ字符串和ZMQ消息中的一帧是等价的,就如上图所展现的,由一个长度属性和一串字节表示。 273 | 274 | 下面这个功能函数会帮助我们在C语言中正确的接受字符串消息: 275 | 276 | ```c 277 | // 从ZMQ套接字中接收字符串,并转换为C语言的字符串 278 | static char * 279 | s_recv (void *socket) { 280 | zmq_msg_t message; 281 | zmq_msg_init (&message); 282 | zmq_recv (socket, &message, 0); 283 | int size = zmq_msg_size (&message); 284 | char *string = malloc (size + 1); 285 | memcpy (string, zmq_msg_data (&message), size); 286 | zmq_msg_close (&message); 287 | string [size] = 0; 288 | return (string); 289 | } 290 | ``` 291 | 292 | 这段代码我们会在日后的示例中使用,我们可以顺手写一个s_send()方法,并打包成一个.h文件供我们使用。 293 | 294 | 这就诞生了zhelpers.h,一个供C语言使用的ZMQ功能函数库。它的源代码比较长,而且只对C语言程序员有用,你可以在闲暇时[看一看](https://github.com/imatix/zguide/blob/master/examples/C/zhelpers.h)。 295 | 296 | ### 获取版本号 297 | 298 | ZMQ目前有多个版本,而且仍在持续更新。如果你遇到了问题,也许这在下一个版本中已经解决了。想知道目前的ZMQ版本,你可以在程序中运行如下: 299 | 300 | **version: ØMQ version reporting in C** 301 | 302 | ```c 303 | // 304 | // 返回当前ZMQ的版本号 305 | // 306 | #include "zhelpers.h" 307 | 308 | int main (void) 309 | { 310 | int major, minor, patch; 311 | zmq_version (&major, &minor, &patch); 312 | printf ("当前ZMQ版本号为 %d.%d.%d\n", major, minor, patch); 313 | 314 | return EXIT_SUCCESS; 315 | } 316 | ``` 317 | 318 | ### 让消息流动起来 319 | 320 | 第二种经典的消息模式是单向数据分发:服务端将更新事件发送给一组客户端。让我们看一个天气信息发布的例子,包括邮编、温度、相对湿度。我们生成这些随机信息,用来模拟气象站所做的那样。 321 | 322 | 下面是服务端的代码,使用5556端口: 323 | 324 | **wuserver: Weather update server in C** 325 | 326 | ```c 327 | // 328 | // 气象信息更新服务 329 | // 绑定PUB套接字至tcp://*:5556端点 330 | // 发布随机气象信息 331 | // 332 | #include "zhelpers.h" 333 | 334 | int main (void) 335 | { 336 | // 准备上下文和PUB套接字 337 | void *context = zmq_init (1); 338 | void *publisher = zmq_socket (context, ZMQ_PUB); 339 | zmq_bind (publisher, "tcp://*:5556"); 340 | zmq_bind (publisher, "ipc://weather.ipc"); 341 | 342 | // 初始化随机数生成器 343 | srandom ((unsigned) time (NULL)); 344 | while (1) { 345 | // 生成数据 346 | int zipcode, temperature, relhumidity; 347 | zipcode = randof (100000); 348 | temperature = randof (215) - 80; 349 | relhumidity = randof (50) + 10; 350 | 351 | // 向所有订阅者发送消息 352 | char update [20]; 353 | sprintf (update, "%05d %d %d", zipcode, temperature, relhumidity); 354 | s_send (publisher, update); 355 | } 356 | zmq_close (publisher); 357 | zmq_term (context); 358 | return 0; 359 | } 360 | ``` 361 | 362 | 这项更新服务没有开始、没有结束,就像永不消失的电波一样。 363 | 364 | ![4](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_4.png) 365 | 366 | 下面是客户端程序,它会接受发布者的消息,只处理特定邮编标注的信息,如纽约的邮编是10001: 367 | 368 | **wuclient: Weather update client in C** 369 | 370 | ```c 371 | // 372 | // 气象信息客户端 373 | // 连接SUB套接字至tcp://*:5556端点 374 | // 收集指定邮编的气象信息,并计算平均温度 375 | // 376 | #include "zhelpers.h" 377 | 378 | int main (int argc, char *argv []) 379 | { 380 | void *context = zmq_init (1); 381 | 382 | // 创建连接至服务端的套接字 383 | printf ("正在收集气象信息...\n"); 384 | void *subscriber = zmq_socket (context, ZMQ_SUB); 385 | zmq_connect (subscriber, "tcp://localhost:5556"); 386 | 387 | // 设置订阅信息,默认为纽约,邮编10001 388 | char *filter = (argc > 1)? argv [1]: "10001 "; 389 | zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, filter, strlen (filter)); 390 | 391 | // 处理100条更新信息 392 | int update_nbr; 393 | long total_temp = 0; 394 | for (update_nbr = 0; update_nbr < 100; update_nbr++) { 395 | char *string = s_recv (subscriber); 396 | int zipcode, temperature, relhumidity; 397 | sscanf (string, "%d %d %d", 398 | &zipcode, &temperature, &relhumidity); 399 | total_temp += temperature; 400 | free (string); 401 | } 402 | printf ("地区邮编 '%s' 的平均温度为 %dF\n", 403 | filter, (int) (total_temp / update_nbr)); 404 | 405 | zmq_close (subscriber); 406 | zmq_term (context); 407 | return 0; 408 | } 409 | ``` 410 | 411 | 需要注意的是,在使用SUB套接字时,必须使用zmq_setsockopt()方法来设置订阅的内容。如果你不设置订阅内容,那将什么消息都收不到,新手很容易犯这个错误。订阅信息可以是任何字符串,可以设置多次。只要消息满足其中一条订阅信息,SUB套接字就会收到。订阅者可以选择不接收某类消息,也是通过zmq_setsockopt()方法实现的。 412 | 413 | PUB-SUB套接字组合是异步的。客户端在一个循环体中使用zmq_recv()接收消息,如果向SUB套接字发送消息则会报错;类似地,服务端可以不断地使用zmq_send()发送消息,但不能在PUB套接字上使用zmq_recv()。 414 | 415 | 关于PUB-SUB套接字,还有一点需要注意:你无法得知SUB是何时开始接收消息的。就算你先打开了SUB套接字,后打开PUB发送消息,这时SUB还是会丢失一些消息的,因为建立连接是需要一些时间的。很少,但并不是零。 416 | 417 | 这种“慢连接”的症状一开始会让很多人困惑,所以这里我要详细解释一下。还记得ZMQ是在后台进行异步的I/O传输的,如果你有两个节点用以下顺序相连: 418 | 419 | * 订阅者连接至端点接收消息并计数; 420 | * 发布者绑定至端点并立刻发送1000条消息。 421 | 422 | 运行的结果很可能是订阅者一条消息都收不到。这时你可能会傻眼,忙于检查有没有设置订阅信息,并重新尝试,但结果还是一样。 423 | 424 | 我们知道在建立TCP连接时需要进行三次握手,会耗费几毫秒的时间,而当节点数增加时这个数字也会上升。在这么短的时间里,ZMQ就可以发送很多很多消息了。举例来说,如果建立连接需要耗时5毫秒,而ZMQ只需要1毫秒就可以发送完这1000条消息。 425 | 426 | 第二章中我会解释如何使发布者和订阅者同步,只有当订阅者准备好时发布者才会开始发送消息。有一种简单的方法来同步PUB和SUB,就是让PUB延迟一段时间再发送消息。现实编程中我不建议使用这种方式,因为它太脆弱了,而且不好控制。不过这里我们先暂且使用sleep的方式来解决,等到第二章的时候再讲述正确的处理方式。 427 | 428 | 另一种同步的方式则是认为发布者的消息流是无穷无尽的,因此丢失了前面一部分信息也没有关系。我们的气象信息客户端就是这么做的。 429 | 430 | 示例中的气象信息客户端会收集指定邮编的一千条信息,其间大约有1000万条信息被发布。你可以先打开客户端,再打开服务端,工作一段时间后重启服务端,这时客户端仍会正常工作。当客户端收集完所需信息后,会计算并输出平均温度。 431 | 432 | 关于发布-订阅模式的几点说明: 433 | 434 | * 订阅者可以连接多个发布者,轮流接收消息; 435 | * 如果发布者没有订阅者与之相连,那它发送的消息将直接被丢弃; 436 | * 如果你使用TCP协议,那当订阅者处理速度过慢时,消息会在发布者处堆积。以后我们会讨论如何使用阈值(HWM)来保护发布者。 437 | * 在目前版本的ZMQ中,消息的过滤是在订阅者处进行的。也就是说,发布者会向订阅者发送所有的消息,订阅者会将未订阅的消息丢弃。 438 | 439 | 我在自己的四核计算机上尝试发布1000万条消息,速度很快,但没什么特别的: 440 | 441 | ``` 442 | ph@ws200901:~/work/git/0MQGuide/examples/c$ time wuclient 443 | Collecting updates from weather server... 444 | Average temperature for zipcode '10001 ' was 18F 445 | 446 | real 0m5.939s 447 | user 0m1.590s 448 | sys 0m2.290s 449 | ``` 450 | 451 | ### 分布式处理 452 | 453 | 下面一个示例程序中,我们将使用ZMQ进行超级计算,也就是并行处理模型: 454 | 455 | * 任务分发器会生成大量可以并行计算的任务; 456 | * 有一组worker会处理这些任务; 457 | * 结果收集器会在末端接收所有worker的处理结果,进行汇总。 458 | 459 | 现实中,worker可能散落在不同的计算机中,利用GPU(图像处理单元)进行复杂计算。下面是任务分发器的代码,它会生成100个任务,任务内容是让收到的worker延迟若干毫秒。 460 | 461 | **taskvent: Parallel task ventilator in C** 462 | 463 | ```c 464 | // 465 | // 任务分发器 466 | // 绑定PUSH套接字至tcp://localhost:5557端点 467 | // 发送一组任务给已建立连接的worker 468 | // 469 | #include "zhelpers.h" 470 | 471 | int main (void) 472 | { 473 | void *context = zmq_init (1); 474 | 475 | // 用于发送消息的套接字 476 | void *sender = zmq_socket (context, ZMQ_PUSH); 477 | zmq_bind (sender, "tcp://*:5557"); 478 | 479 | // 用于发送开始信号的套接字 480 | void *sink = zmq_socket (context, ZMQ_PUSH); 481 | zmq_connect (sink, "tcp://localhost:5558"); 482 | 483 | printf ("准备好worker后按任意键开始: "); 484 | getchar (); 485 | printf ("正在向worker分配任务...\n"); 486 | 487 | // 发送开始信号 488 | s_send (sink, "0"); 489 | 490 | // 初始化随机数生成器 491 | srandom ((unsigned) time (NULL)); 492 | 493 | // 发送100个任务 494 | int task_nbr; 495 | int total_msec = 0; // 预计执行时间(毫秒) 496 | for (task_nbr = 0; task_nbr < 100; task_nbr++) { 497 | int workload; 498 | // 随机产生1-100毫秒的工作量 499 | workload = randof (100) + 1; 500 | total_msec += workload; 501 | char string [10]; 502 | sprintf (string, "%d", workload); 503 | s_send (sender, string); 504 | } 505 | printf ("预计执行时间: %d 毫秒\n", total_msec); 506 | sleep (1); // 延迟一段时间,让任务分发完成 507 | 508 | zmq_close (sink); 509 | zmq_close (sender); 510 | zmq_term (context); 511 | return 0; 512 | } 513 | ``` 514 | 515 | ![5](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_5.png) 516 | 517 | 下面是worker的代码,它接受信息并延迟指定的毫秒数,并发送执行完毕的信号: 518 | 519 | **taskwork: Parallel task worker in C** 520 | 521 | ```c 522 | // 523 | // 任务执行器 524 | // 连接PULL套接字至tcp://localhost:5557端点 525 | // 从任务分发器处获取任务 526 | // 连接PUSH套接字至tcp://localhost:5558端点 527 | // 向结果采集器发送结果 528 | // 529 | #include "zhelpers.h" 530 | 531 | int main (void) 532 | { 533 | void *context = zmq_init (1); 534 | 535 | // 获取任务的套接字 536 | void *receiver = zmq_socket (context, ZMQ_PULL); 537 | zmq_connect (receiver, "tcp://localhost:5557"); 538 | 539 | // 发送结果的套接字 540 | void *sender = zmq_socket (context, ZMQ_PUSH); 541 | zmq_connect (sender, "tcp://localhost:5558"); 542 | 543 | // 循环处理任务 544 | while (1) { 545 | char *string = s_recv (receiver); 546 | // 输出处理进度 547 | fflush (stdout); 548 | printf ("%s.", string); 549 | 550 | // 开始处理 551 | s_sleep (atoi (string)); 552 | free (string); 553 | 554 | // 发送结果 555 | s_send (sender, ""); 556 | } 557 | zmq_close (receiver); 558 | zmq_close (sender); 559 | zmq_term (context); 560 | return 0; 561 | } 562 | ``` 563 | 564 | 下面是结果收集器的代码。它会收集100个处理结果,并计算总的执行时间,让我们由此判别任务是否是并行计算的。 565 | 566 | **tasksink: Parallel task sink in C** 567 | 568 | ```c 569 | // 570 | // 任务收集器 571 | // 绑定PULL套接字至tcp://localhost:5558端点 572 | // 从worker处收集处理结果 573 | // 574 | #include "zhelpers.h" 575 | 576 | int main (void) 577 | { 578 | // 准备上下文和套接字 579 | void *context = zmq_init (1); 580 | void *receiver = zmq_socket (context, ZMQ_PULL); 581 | zmq_bind (receiver, "tcp://*:5558"); 582 | 583 | // 等待开始信号 584 | char *string = s_recv (receiver); 585 | free (string); 586 | 587 | // 开始计时 588 | int64_t start_time = s_clock (); 589 | 590 | // 确定100个任务均已处理 591 | int task_nbr; 592 | for (task_nbr = 0; task_nbr < 100; task_nbr++) { 593 | char *string = s_recv (receiver); 594 | free (string); 595 | if ((task_nbr / 10) * 10 == task_nbr) 596 | printf (":"); 597 | else 598 | printf ("."); 599 | fflush (stdout); 600 | } 601 | // 计算并输出总执行时间 602 | printf ("执行时间: %d 毫秒\n", 603 | (int) (s_clock () - start_time)); 604 | 605 | zmq_close (receiver); 606 | zmq_term (context); 607 | return 0; 608 | } 609 | ``` 610 | 611 | 一组任务的平均执行时间在5秒左右,以下是分别开始1个、2个、4个worker时的执行结果: 612 | 613 | ``` 614 | # 1 worker 615 | Total elapsed time: 5034 msec 616 | # 2 workers 617 | Total elapsed time: 2421 msec 618 | # 4 workers 619 | Total elapsed time: 1018 msec 620 | ``` 621 | 622 | 关于这段代码的几个细节: 623 | 624 | * worker上游和任务分发器相连,下游和结果收集器相连,这就意味着你可以开启任意多个worker。但若worker是绑定至端点的,而非连接至端点,那我们就需要准备更多的端点,并配置任务分发器和结果收集器。所以说,任务分发器和结果收集器是这个网络结构中较为稳定的部分,因此应该由它们绑定至端点,而非worker,因为它们较为动态。 625 | 626 | * 我们需要做一些同步的工作,等待worker全部启动之后再分发任务。这点在ZMQ中很重要,且不易解决。连接套接字的动作会耗费一定的时间,因此当第一个worker连接成功时,它会一下收到很多任务。所以说,如果我们不进行同步,那这些任务根本就不会被并行地执行。你可以自己试验一下。 627 | 628 | * 任务分发器使用PUSH套接字向worker均匀地分发任务(假设所有的worker都已经连接上了),这种机制称为_负载均衡_,以后我们会见得更多。 629 | 630 | * 结果收集器的PULL套接字会均匀地从worker处收集消息,这种机制称为_公平队列_: 631 | 632 | ![6](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_6.png) 633 | 634 | 管道模式也会出现慢连接的情况,让人误以为PUSH套接字没有进行负载均衡。如果你的程序中某个worker接收到了更多的请求,那是因为它的PULL套接字连接得比较快,从而在别的worker连接之前获取了额外的消息。 635 | 636 | ### 使用ZMQ编程 637 | 638 | 看着这些示例程序后,你一定迫不及待想要用ZMQ进行编程了。不过在开始之前,我还有几条建议想给到你,这样可以省去未来的一些麻烦: 639 | 640 | * 学习ZMQ要循序渐进,虽然它只是一套API,但却提供了无尽的可能。一步一步学习它提供的功能,并完全掌握。 641 | 642 | * 编写漂亮的代码。丑陋的代码会隐藏问题,让想要帮助你的人无从下手。比如,你会习惯于使用无意义的变量名,但读你代码的人并不知道。应使用有意义的变量名称,而不是随意起一个。代码的缩进要统一,布局清晰。漂亮的代码可以让你的世界变得更美好。 643 | 644 | * 边写边测试,当代码出现问题,你就可以快速定位到某些行。这一点在编写ZMQ应用程序时尤为重要,因为很多时候你无法第一次就编写出正确的代码。 645 | 646 | * 当你发现自己编写的代码无法正常工作时,你可以将其拆分成一些代码片段,看看哪段没有正确地执行。ZMQ可以让你构建非常模块化的代码,所以应该好好利用这一点。 647 | 648 | * 需要时应使用抽象的方法来编写程序(类、成员函数等等),不要随意拷贝代码,因为拷贝代码的同时也是在拷贝错误。 649 | 650 | 我们看看下面这段代码,是某位同仁让我帮忙修改的: 651 | 652 | ```c 653 | // 注意:不要使用这段代码! 654 | static char *topic_str = "msg.x|"; 655 | 656 | void* pub_worker(void* arg){ 657 | void *ctx = arg; 658 | assert(ctx); 659 | 660 | void *qskt = zmq_socket(ctx, ZMQ_REP); 661 | assert(qskt); 662 | 663 | int rc = zmq_connect(qskt, "inproc://querys"); 664 | assert(rc == 0); 665 | 666 | void *pubskt = zmq_socket(ctx, ZMQ_PUB); 667 | assert(pubskt); 668 | 669 | rc = zmq_bind(pubskt, "inproc://publish"); 670 | assert(rc == 0); 671 | 672 | uint8_t cmd; 673 | uint32_t nb; 674 | zmq_msg_t topic_msg, cmd_msg, nb_msg, resp_msg; 675 | 676 | zmq_msg_init_data(&topic_msg, topic_str, strlen(topic_str) , NULL, NULL); 677 | 678 | fprintf(stdout,"WORKER: ready to recieve messages\n"); 679 | // 注意:不要使用这段代码,它不能工作! 680 | // e.g. topic_msg will be invalid the second time through 681 | while (1){ 682 | zmq_send(pubskt, &topic_msg, ZMQ_SNDMORE); 683 | 684 | zmq_msg_init(&cmd_msg); 685 | zmq_recv(qskt, &cmd_msg, 0); 686 | memcpy(&cmd, zmq_msg_data(&cmd_msg), sizeof(uint8_t)); 687 | zmq_send(pubskt, &cmd_msg, ZMQ_SNDMORE); 688 | zmq_msg_close(&cmd_msg); 689 | 690 | fprintf(stdout, "recieved cmd %u\n", cmd); 691 | 692 | zmq_msg_init(&nb_msg); 693 | zmq_recv(qskt, &nb_msg, 0); 694 | memcpy(&nb, zmq_msg_data(&nb_msg), sizeof(uint32_t)); 695 | zmq_send(pubskt, &nb_msg, 0); 696 | zmq_msg_close(&nb_msg); 697 | 698 | fprintf(stdout, "recieved nb %u\n", nb); 699 | 700 | zmq_msg_init_size(&resp_msg, sizeof(uint8_t)); 701 | memset(zmq_msg_data(&resp_msg), 0, sizeof(uint8_t)); 702 | zmq_send(qskt, &resp_msg, 0); 703 | zmq_msg_close(&resp_msg); 704 | 705 | } 706 | return NULL; 707 | } 708 | ``` 709 | 710 | 下面是我为他重写的代码,顺便修复了一些BUG: 711 | 712 | ```c 713 | static void * 714 | worker_thread (void *arg) { 715 | void *context = arg; 716 | void *worker = zmq_socket (context, ZMQ_REP); 717 | assert (worker); 718 | int rc; 719 | rc = zmq_connect (worker, "ipc://worker"); 720 | assert (rc == 0); 721 | 722 | void *broadcast = zmq_socket (context, ZMQ_PUB); 723 | assert (broadcast); 724 | rc = zmq_bind (broadcast, "ipc://publish"); 725 | assert (rc == 0); 726 | 727 | while (1) { 728 | char *part1 = s_recv (worker); 729 | char *part2 = s_recv (worker); 730 | printf ("Worker got [%s][%s]\n", part1, part2); 731 | s_sendmore (broadcast, "msg"); 732 | s_sendmore (broadcast, part1); 733 | s_send (broadcast, part2); 734 | free (part1); 735 | free (part2); 736 | 737 | s_send (worker, "OK"); 738 | } 739 | return NULL; 740 | } 741 | ``` 742 | 743 | 上段程序的最后,它将套接字在两个线程之间传递,这会导致莫名其妙的问题。这种行为在ZMQ 2.1中虽然是合法的,但是不提倡使用。 744 | 745 | ### ZMQ 2.1版 746 | 747 | 历史告诉我们,ZMQ 2.0是一个低延迟的分布式消息系统,它从众多同类软件中脱颖而出,摆脱了各种奢华的名目,向世界宣告“无极限”的口号。这是我们一直在使用的稳定发行版。 748 | 749 | 时过境迁,2010年流行的东西在2011年就不一定了。当ZMQ的开发者和社区开发者在激烈地讨论ZMQ的种种问题时,ZMQ 2.1横空出世了,成为新的稳定发行版。 750 | 751 | 本指南主要针对ZMQ 2.1进行描述,因此对于从ZMQ 2.0迁移过来的开发者来说有一些需要注意的地方: 752 | 753 | * 在2.0中,调用zmq_close()和zmq_term()时会丢弃所有尚未发送的消息,所以在发送完消息后不能直接关闭程序,2.0的示例中往往使用sleep(1)来规避这个问题。但是在2.1中就不需要这样做了,程序会等待消息全部发送完毕后再退出。 754 | 755 | * 相反地,2.0中可以在尚有套接字打开的情况下调用zmq_term(),这在2.1中会变得不安全,会造成程序的阻塞。所以,在2.1程序中我们_会先关闭所有的套接字_,然后才退出程序。如果套接字中有尚未发送的消息,程序就会一直处于等待状态,_除非手工设置了套接字的LINGER选项_(如设置为零),那么套接字会在相应的时间后关闭。 756 | 757 | ```c 758 | int zero = 0; 759 | zmq_setsockopt (mysocket, ZMQ_LINGER, &zero, sizeof (zero)); 760 | ``` 761 | 762 | * 2.0中,zmq_poll()函数没有定时功能,它会在满足条件时立刻返回,我们需要在循环体中检查还有多少剩余。但在2.1中,zmq_poll()会在指定时间后返回,因此可以作为定时器使用。 763 | 764 | * 2.0中,ZMQ会忽略系统的中断消息,这就意味着对libzmq的调用是不会收到EINTR消息的,这样就无法对SIGINT(Ctrl-C)等消息进行处理了。在2.1中,这个问题得以解决,像类似zmq_recv()的方法都会接收并返回系统的EINTR消息。 765 | 766 | ### 正确地使用上下文 767 | 768 | ZMQ应用程序的一开始总是会先创建一个上下文,并用它来创建套接字。在C语言中,创建上下文的函数是zmq_init()。一个进程中只应该创建一个上下文。从技术的角度来说,上下文是一个容器,包含了该进程下所有的套接字,并为inproc协议提供实现,用以高速连接进程内不同的线程。如果一个进程中创建了两个上下文,那就相当于启动了两个ZMQ实例。如果这正是你需要的,那没有问题,但一般情况下: 769 | 770 | **在一个进程中使用zmq_init()函数创建一个上下文,并在结束时使用zmq_term()函数关闭它** 771 | 772 | 如果你使用了fork()系统调用,那每个进程需要自己的上下文对象。如果在调用fork()之前调用了zmq_init()函数,那每个子进程都会有自己的上下文对象。通常情况下,你会需要在子进程中做些有趣的事,而让父进程来管理它们。 773 | 774 | ### 正确地退出和清理 775 | 776 | 程序员的一个良好习惯是:总是在结束时进行清理工作。当你使用像Python那样的语言编写ZMQ应用程序时,系统会自动帮你完成清理。但如果使用的是C语言,那就需要小心地处理了,否则可能发生内存泄露、应用程序不稳定等问题。 777 | 778 | 内存泄露只是问题之一,其实ZMQ是很在意程序的退出方式的。个中原因比较复杂,但简单的来说,如果仍有套接字处于打开状态,调用zmq_term()时会导致程序挂起;就算关闭了所有的套接字,如果仍有消息处于待发送状态,zmq_term()也会造成程序的等待。只有当套接字的LINGER选项设为0时才能避免。 779 | 780 | 我们需要关注的ZMQ对象包括:消息、套接字、上下文。好在内容并不多,至少在一般的应用程序中是这样: 781 | 782 | * 处理完消息后,记得用zmq_msg_close()函数关闭消息; 783 | * 如果你同时打开或关闭了很多套接字,那可能需要重新规划一下程序的结构了; 784 | * 退出程序时,应该先关闭所有的套接字,最后调用zmq_term()函数,销毁上下文对象。 785 | 786 | 如果要用ZMQ进行多线程的编程,需要考虑的问题就更多了。我们会在下一章中详述多线程编程,但如果你耐不住性子想要尝试一下,以下是在退出时的一些建议: 787 | 788 | * 不要在多个线程中使用同一个套接字。不要去想为什么,反正别这么干就是了。 789 | * 关闭所有的套接字,并在主程序中关闭上下文对象。 790 | * 如果仍有处于阻塞状态的recv或poll调用,应该在主程序中捕捉这些错误,并在相应的线程中关闭套接字。不要重复关闭上下文,zmq_term()函数会等待所有的套接字安全地关闭后才结束。 791 | 792 | 看吧,过程是复杂的,所以不同语言的API实现者可能会将这些步骤封装起来,让结束程序变得不那么复杂。 793 | 794 | ### 我们为什么需要ZMQ 795 | 796 | 现在我们已经将ZMQ运行起来了,让我们回顾一下为什么我们需要ZMQ: 797 | 798 | 目前的应用程序很多都会包含跨网络的组件,无论是局域网还是因特网。这些程序的开发者都会用到某种消息通信机制。有些人会使用某种消息队列产品,而大多数人则会自己手工来做这些事,使用TCP或UDP协议。这些协议使用起来并不困难,但是,简单地将消息从A发给B,和在任何情况下都能进行可靠的消息传输,这两种情况显然是不同的。 799 | 800 | 让我们看看在使用纯TCP协议进行消息传输时会遇到的一些典型问题。任何可复用的消息传输层肯定或多或少地会要解决以下问题: 801 | 802 | * 如何处理I/O?是让程序阻塞等待响应,还是在后台处理这些事?这是软件设计的关键因素。阻塞式的I/O操作会让程序架构难以扩展,而后台处理I/O也是比较困难的。 803 | 804 | * 如何处理那些临时的、来去自由的组件?我们是否要将组件分为客户端和服务端两种,并要求服务端永不消失?那如果我们想要将服务端相连怎么办?我们要每隔几秒就进行重连吗? 805 | 806 | * 我们如何表示一条消息?我们怎样通过拆分消息,让其变得易读易写,不用担心缓存溢出,既能高效地传输小消息,又能胜任视频等大型文件的传输? 807 | 808 | * 如何处理那些不能立刻发送出去的消息?比如我们需要等待一个网络组件重新连接的时候?我们是直接丢弃该条消息,还是将它存入数据库,或是内存中的一个队列? 809 | 810 | * 要在哪里保存消息队列?如果某个组件读取消息队列的速度很慢,造成消息的堆积怎么办?我们要采取什么样的策略? 811 | 812 | * 如何处理丢失的消息?我们是等待新的数据,请求重发,还是需要建立一套新的可靠性机制以保证消息不会丢失?如果这个机制自身崩溃了呢? 813 | 814 | * 如果我们想换一种网络连接协议,如用广播代替TCP单播?或者改用IPv6?我们是否需要重写所有的应用程序,或者将这种协议抽象到一个单独的层中? 815 | 816 | * 我们如何对消息进行路由?我们可以将消息同时发送给多个节点吗?是否能将应答消息返回给请求的发送方? 817 | 818 | * 我们如何为另一种语言写一个API?我们是否需要完全重写某项协议,还是重新打包一个类库? 819 | 820 | * 怎样才能做到在不同的架构之间传送消息?是否需要为消息规定一种编码? 821 | 822 | * 我们如何处理网络通信错误?等待并重试,还是直接忽略或取消? 823 | 824 | 我们可以找一个开源软件来做例子,如[Hadoop Zookeeper](http://hadoop.apache.org/zookeeper/),看一下它的C语言API源码,[src/c/src/zookeeper.c]([http://github.com/apache/zookeeper/blob/trunk/src/c/src/zookeeper.c src/c/src/zookeeper.c)。这段代码大约有3200行,没有注释,实现了一个C/S网络通信协议。它工作起来很高效,因为使用了poll()来代替select()。但是,Zookeeper应该被抽象出来,作为一种通用的消息通信层,并加以详细的注释。像这样的模块应该得到最大程度上的复用,而不是重复地制造轮子。 825 | 826 | ![7](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_7.png) 827 | 828 | 但是,如何编写这样一个可复用的消息层呢?为什么长久以来人们宁愿在自己的代码中重复书写控制原始TCP套接字的代码,而不愿编写这样一个公共库呢? 829 | 830 | 其实,要编写一个通用的消息层是件非常困难的事,这也是为什么FOSS项目不断在尝试,一些商业化的消息产品如此之复杂、昂贵、僵硬、脆弱。2006年,iMatix设计了AMQP协议,为FOSS项目的开发者提供了可能是当时第一个可复用的消息系统。[AMQP][]比其他同类产品要来得好,但[仍然是复杂、昂贵和脆弱的](http://www.imatix.com/articles:whats-wrong-with-amqp)。它需要花费几周的时间去学习,花费数月的时间去创建一个真正能用的架构,到那时可能为时已晚了。 831 | 832 | 大多数消息系统项目,如AMQP,为了解决上面提到的种种问题,发明了一些新的概念,如“代理”的概念,将寻址、路由、队列等功能都包含了进来。结果就是在一个没有任何注释的协议之上,又构建了一个C/S协议和相应的API,让应用程序和代理相互通信。代理的确是一个不错的解决方案,帮助降低大型网络结构的复杂度。但是,在Zookeeper这样的项目中应用代理机制的消息系统,可能是件更加糟糕的事,因为这意味了需要添加一台新的计算机,并构成一个新的单点故障。代理会逐渐成为新的瓶颈,管理起来更具风险。如果软件支持,我们可以添加第二个、第三个、第四个代理,构成某种冗余容错的模式。有人就是这么做的,这让系统架构变得更为复杂,增加了隐患。 833 | 834 | 在这种以代理为中心的架构下,需要一支专门的运维团队。你需要昼夜不停地观察代理的状态,不时地用棍棒调教他们。你需要添加计算机,以及更多的备份机,你需要有专人管理这些机器。这样做只对那些大型的网络应用程序才有意义,因为他们有更多可移动的模块,有多个团队进行开发和维护,而且已经经过了多年的建设。 835 | 836 | 这样一来,中小应用程序的开发者们就无计可施了。他们只能设法避免编写网络应用程序,转而编写那些不需要扩展的程序;或者可以使用原始的方式进行网络编程,但编写的软件会非常脆弱和复杂,难以维护;亦或者他们选择一种消息通信产品,虽然能够开发出扩展性强的应用程序,但需要支付高昂的代价。似乎没有一种选择是合理的,这也是为什么在上个世纪消息系统会成为一个广泛的问题。 837 | 838 | ![8](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_8.png) 839 | 840 | 我们真正需要的是这样一种消息软件,它能够做大型消息软件所能做的一切,但使用起来又非常简单,成本很低,可以用到所有的应用程序中,没有任何依赖条件。因为没有了额外的模块,就降低了出错的概率。这种软件需要能够在所有的操作系统上运行,并能支持所有的编程语言。 841 | 842 | ZMQ就是这样一种软件:它高效,提供了嵌入式的类库,使应用程序能够很好地在网络中扩展,成本低廉。 843 | 844 | ZMQ的主要特点有: 845 | 846 | * ZMQ会在后台线程异步地处理I/O操作,它使用一种不会死锁的数据结构来存储消息。 847 | * 网络组件可以来去自如,ZMQ会负责自动重连,这就意味着你可以以任何顺序启动组件;用它创建的面向服务架构(SOA)中,服务端可以随意地加入或退出网络。 848 | * ZMQ会在有必要的情况下自动将消息放入队列中保存,一旦建立了连接就开始发送。 849 | * ZMQ有阈值(HWM)的机制,可以避免消息溢出。当队列已满,ZMQ会自动阻塞发送者,或丢弃部分消息,这些行为取决于你所使用的消息模式。 850 | * ZMQ可以让你用不同的通信协议进行连接,如TCP、广播、进程内、进程间。改变通信协议时你不需要去修改代码。 851 | * ZMQ会恰当地处理速度较慢的节点,会根据消息模式使用不同的策略。 852 | * ZMQ提供了多种模式进行消息路由,如请求-应答模式、发布-订阅模式等。这些模式可以用来搭建网络拓扑结构。 853 | * ZMQ中可以根据消息模式建立起一些中间装置(很小巧),可以用来降低网络的复杂程度。 854 | * ZMQ会发送整个消息,使用消息帧的机制来传递。如果你发送了10KB大小的消息,你就会收到10KB大小的消息。 855 | * ZMQ不强制使用某种消息格式,消息可以是0字节的,或是大到GB级的数据。当你表示这些消息时,可以选用诸如谷歌的protocol buffers,XDR等序列化产品。 856 | * ZMQ能够智能地处理网络错误,有时它会进行重试,有时会告知你某项操作发生了错误。 857 | * ZMQ甚至可以降低对环境的污染,因为节省了CPU时间意味着节省了电能。 858 | 859 | 其实ZMQ可以做的还不止这些,它会颠覆人们编写网络应用程序的模式。虽然从表面上看,它不过是提供了一套处理套接字的API,能够用zmq_recv()和zmq_send()进行消息的收发,但是,消息处理将成为应用程序的核心部分,很快你的程序就会变成一个个消息处理模块,这既美观又自然。它的扩展性还很强,每项任务由一个节点(节点是一个线程)、同一台机器上的两个节点(节点是一个进程)、同一网络上的两台机器(节点是一台机器)来处理,而不需要改动应用程序。 860 | 861 | ### 套接字的扩展性 862 | 863 | 我们来用实例看看ZMQ套接字的扩展性。这个脚本会启动气象信息服务及多个客户端: 864 | 865 | ``` 866 | wuserver & 867 | wuclient 12345 & 868 | wuclient 23456 & 869 | wuclient 34567 & 870 | wuclient 45678 & 871 | wuclient 56789 & 872 | ``` 873 | 874 | 执行过程中,我们可以通过top命令查看进程状态(以下是一台四核机器的情况): 875 | 876 | ``` 877 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 878 | 7136 ph 20 0 1040m 959m 1156 R 157 12.0 16:25.47 wuserver 879 | 7966 ph 20 0 98608 1804 1372 S 33 0.0 0:03.94 wuclient 880 | 7963 ph 20 0 33116 1748 1372 S 14 0.0 0:00.76 wuclient 881 | 7965 ph 20 0 33116 1784 1372 S 6 0.0 0:00.47 wuclient 882 | 7964 ph 20 0 33116 1788 1372 S 5 0.0 0:00.25 wuclient 883 | 7967 ph 20 0 33072 1740 1372 S 5 0.0 0:00.35 wuclient 884 | ``` 885 | 886 | 我们想想现在发生了什么:气象信息服务程序有一个单独的套接字,却能同时向五个客户端并行地发送消息。我们可以有成百上千个客户端并行地运作,服务端看不到这些客户端,不能操纵它们。 887 | 888 | ### 如果解决丢失消息的问题 889 | 890 | 在编写ZMQ应用程序时,你遇到最多的问题可能是无法获得消息。下面有一个问题解决路线图,列举了最基本的出错原因。不用担心其中的某些术语你没有见过,在后面的几章里都会讲到。 891 | 892 | ![9](https://github.com/anjuke/zguide-cn/raw/master/images/chapter1_9.png) 893 | 894 | 如果ZMQ在你的应用程序中扮演非常重要的角色,那你可能就需要好好计划一下了。首先,创建一个原型,用以测试设计方案的可行性。采取一些压力测试的手段,确保它足够的健壮。其次,主攻测试代码,也就是编写测试框架,保证有足够的电力供应和时间,来进行高强度的测试。理想状态下,应该由一个团队编写程序,另一个团队负责击垮它。最后,让你的公司及时[联系iMatix](http://www.imatix.com/contact),获得技术上的支持。 895 | 896 | 简而言之,如果你没有足够理由说明设计出来的架构能够在现实环境中运行,那么很有可能它就会在最紧要的关头崩溃。 897 | 898 | ### 警告:你的想法可能会被颠覆! 899 | 900 | 传统网络编程的一个规则是套接字只能和一个节点建立连接。虽然也有广播的协议,但毕竟是第三方的。当我们认定“一个套接字 = 一个连接”的时候,我们会用一些特定的方式来扩展应用程序架构:我们为每一块逻辑创建线程,该线程独立地维护一个套接字。 901 | 902 | 但在ZMQ的世界里,套接字是智能的、多线程的,能够自动地维护一组完整的连接。你无法看到它们,甚至不能直接操纵这些连接。当你进行消息的收发、轮询等操作时,只能和ZMQ套接字打交道,而不是连接本身。所以说,ZMQ世界里的连接是私有的,不对外部开放,这也是ZMQ易于扩展的原因之一。 903 | 904 | 由于你的代码只会和某个套接字进行通信,这样就可以处理任意多个连接,使用任意一种网络协议。而ZMQ的消息模式又可以进行更为廉价和便捷的扩展。 905 | 906 | 这样一来,传统的思维就无法在ZMQ的世界里应用了。在你阅读示例程序代码的时候,也许你脑子里会想方设法地将这些代码和传统的网络编程相关联:当你读到“套接字”的时候,会认为它就表示与另一个节点的连接——这种想法是错误的;当你读到“线程”时,会认为它是与另一个节点的连接——这也是错误的。 907 | 908 | 如果你是第一次阅读本指南,使用ZMQ进行了一两天的开发(或者更长),可能会觉得疑惑,ZMQ怎么会让事情便得如此简单。你再次尝试用以往的思维去理解ZMQ,但又无功而返。最后,你会被ZMQ的理念所折服,拨云见雾,开始享受ZMQ带来的乐趣。 909 | 910 | [iMatix]: http://www.imatix.com/ 911 | [AMQP]: http://www.amqp.org/ 912 | 913 | -------------------------------------------------------------------------------- /chapter1.txt: -------------------------------------------------------------------------------- 1 | .- vim: set filetype=markdown: 2 | .set GIT=https://github.com/anjuke/zguide-cn 3 | 4 | # ZMQ 指南 5 | 6 | **作者: Pieter Hintjens , CEO iMatix Corporation.** 7 | **翻译: 张吉 , 安居客集团 好租网工程师** 8 | 9 | With thanks to Bill Desmarais, Brian Dorsey, CAF, Daniel Lin, Eric Desgranges, Gonzalo Diethelm, Guido Goldstein, Hunter Ford, Kamil Shakirov, Martin Sustrik, Mike Castleman, Naveen Chawla, Nicola Peduzzi, Oliver Smith, Olivier Chamoux, Peter Alexander, Pierre Rouleau, Randy Dryburgh, John Unwin, Alex Thomas, Mihail Minkov, Jeremy Avnet, Michael Compton, Kamil Kisiel, Mark Kharitonov, Guillaume Aubert, Ian Barber, Mike Sheridan, Faruk Akgul, Oleg Sidorov, Lev Givon, Allister MacLeod, Alexander D'Archangel, Andreas Hoelzlwimmer, Han Holl, Robert G. Jakabosky, Felipe Cruz, Marcus McCurdy, Mikhail Kulemin, Dr. Gergő Érdi, Pavel Zhukov, Alexander Else, Giovanni Ruggiero, Rick "Technoweenie", Daniel Lundin, Dave Hoover, Simon Jefford, Benjamin Peterson, Justin Case, Devon Weller, Richard Smith, Alexander Morland, Wadim Grasza, Michael Jakl, and Zed Shaw for their contributions, and to Stathis Sideris for [Ditaa](http://www.ditaa.org). 10 | 11 | Please use the [issue tracker](https://github.com/imatix/zguide/issues) for all comments and errata. This version covers the latest stable release of 0MQ and was published on &date("ddd d mmmm, yyyy"). 12 | 13 | The Guide is mainly [in C](http://zguide.zeromq.org/page:all), but also in [PHP](http://zguide.zeromq.org/php:all) and [Lua](http://zguide.zeromq.org/lua:all). 14 | 15 | --- 16 | 17 | This work is licensed under a [Creative Commons Attribution-ShareAlike 3.0 License](http://creativecommons.org/licenses/by-sa/3.0/). 18 | 19 | ## 第一章 ZeroMQ基础 20 | 21 | ### 拯救世界 22 | 23 | 如何解释ZMQ?有些人会先说一堆ZMQ的好:它是一套用于快速构建的套接字组件;它的信箱系统有超强的路由能力;它太快了!而有些人则喜欢分享他们被ZMQ点悟的时刻,那些被灵感击中的瞬间:所有的事情突然变得简单明了,让人大开眼界。另一些人则会拿ZMQ同其他产品做个比较:它更小,更简单,但却让人觉得如此熟悉。对于我个人而言,我则更倾向于和别人分享ZMQ的诞生史,相信会和各位读者有所共鸣。 24 | 25 | 编程是一门科学,但往往会乔装成一门艺术。我们从不去了解软件最底层的机理,或者说根本没有人在乎这些。软件并不只是算法、数据结构、编程语言、或者抽象云云,这些不过是一些工具而已,被我们创造、使用、最后抛弃。软件真正的本质,其实是人的本质。 26 | 27 | 举例来说,当我们遇到一个高度复杂的问题时,我们会群策群力,分工合作,将问题拆分为若干个部分,一起解决。这里就体现了编程的科学:创建一组小型的构建模块,让人们易于理解和使用,那么大家就会一起用它来解决问题。 28 | 29 | 我们生活在一个普遍联系的世界里,需要现代的编程软件为我们做指引。所以,未来我们所需要的用于处理大规模计算的构建模块,必须是普遍联系的,而且能够并行运作。那时,程序代码不能再只关注自己,它们需要互相交流,变得足够健谈。程序代码需要像人脑一样,数以兆计的神经元高速地传输信号,在一个没有中央控制的环境下,没有单点故障的环境下,解决问题。这一点其实并不意外,因为就当今的网络来讲,每个节点其实就像是连接了一个人脑一样。 30 | 31 | 如果你曾和线程、协议、或网络打过交道,你会觉得我上面的话像是天方夜谭。因为在实际应用过程中,只是连接几个程序或网络就已经非常困难和麻烦了。数以兆计的节点?那真是无法想象的。现今只有资金雄厚的企业才能负担得起这种软件和服务。 32 | 33 | 当今世界的网络结构已经远远超越了我们自身的驾驭能力。十九世纪八十年代的软件危机,弗莱德•布鲁克斯曾说过,这个世上[没有银弹](http://en.wikipedia.org/wiki/No_Silver_Bullet 34 | )。后来,免费和开源解决了这次软件危机,让我们能够高效地分享知识。如今,我们又面临一次新的软件危机,只不过我们谈论得不多。只有那些大型的、富足的企业才有财力建立高度联系的应用程序。那里有云的存在,但它是私有的。我们的数据和知识正在从我们的个人电脑中消失,流入云端,无法获得或与其竞争。是谁坐拥我们的社交网络?这真像一次巨型主机的革命。 35 | 36 | 我们暂且不谈其中的政治因素,光那些就可以另外出本书了。目前的现状是,虽然互联网能够让千万个程序相连,但我们之中的大多数却无法做到这些。这样一来,那些真正有趣的大型问题(如健康、教育、经济、交通等领域),仍然无法解决。我们没有能力将代码连接起来,也就不能像大脑中的神经元一样处理那些大规模的问题。 37 | 38 | 已经有人尝试用各种方法来连接应用程序,如数以千计的IETF规范,每种规范解决一个特定问题。对于开发人员来说,HTTP协议是比较简单和易用的,但这也往往让问题变得更糟,因为它鼓励人们形成一种重服务端、轻客户端的思想。 39 | 40 | 所以迄今为止人们还在使用原始的TCP/UDP协议、私有协议、HTTP协议、网络套接字等形式连接应用程序。这种做法依旧让人痛苦,速度慢又不易扩展,需要集中化管理。而分布式的P2P协议又仅仅适用于娱乐,而非真正的应用。有谁会使用Skype或者Bittorrent来交换数据呢? 41 | 42 | 这就让我们回归到编程科学的问题上来。想要拯救这个世界,我们需要做两件事情:一,如何在任何地点连接任何两个应用程序;二、将这个解决方案用最为简单的方式包装起来,供程序员使用。 43 | 44 | 也许这听起来太简单了,但事实确实如此。 45 | 46 | ### ZMQ简介 47 | 48 | ZMQ(ØMQ、ZeroMQ, 0MQ)看起来像是一套嵌入式的网络链接库,但工作起来更像是一个并发式的框架。它提供的套接字可以在多种协议中传输消息,如线程间、进程间、TCP、广播等。你可以使用套接字构建多对多的连接模式,如扇出、发布-订阅、任务分发、请求-应答等。ZMQ的快速足以胜任集群应用产品。它的异步I/O机制让你能够构建多核应用程序,完成异步消息处理任务。ZMQ有着多语言支持,并能在几乎所有的操作系统上运行。ZMQ是[iMatix][]公司的产品,以LGPL开源协议发布。 49 | 50 | ### 需要具备的知识 51 | 52 | * 使用最新的ZMQ稳定版本; 53 | * 使用Linux系统或其他相似的操作系统; 54 | * 能够阅读C语言代码,这是本指南示例程序的默认语言; 55 | * 当我们书写诸如PUSH或SUBSCRIBE等常量时,你能够找到相应语言的实现,如ZMQ_PUSH、ZMQ_SUBSCRIBE。 56 | 57 | ### 获取示例 58 | 59 | 本指南的所有示例都存放于[github仓库](https://github.com/imatix/zguide)中,最简单的获取方式是运行以下代码: 60 | 61 | ``` 62 | git clone git://github.com/imatix/zguide.git 63 | ``` 64 | 65 | 浏览examples目录,你可以看到多种语言的实现。如果其中缺少了某种你正在使用的语言,我们很希望你可以[提交一份补充](http://zguide.zeromq.org/main:translate)。这也是本指南实用的原因,要感谢所有做出过贡献的人。 66 | 67 | 所有的示例代码都以MIT/X11协议发布,若在源代码中有其他限定的除外。 68 | 69 | ### 提问-回答 70 | 71 | 让我们从简单的代码开始,一段传统的Hello World程序。我们会创建一个客户端和一个服务端,客户端发送Hello给服务端,服务端返回World。下文是C语言编写的服务端,它在5555端口打开一个ZMQ套接字,等待请求,收到后应答World。 72 | 73 | **hwserver.c: Hello World server** 74 | 75 | ```c 76 | // 77 | // Hello World 服务端 78 | // 绑定一个REP套接字至tcp://*:5555 79 | // 从客户端接收Hello,并应答World 80 | // 81 | #include 82 | #include 83 | #include 84 | #include 85 | 86 | int main (void) 87 | { 88 | void *context = zmq_init (1); 89 | 90 | // 与客户端通信的套接字 91 | void *responder = zmq_socket (context, ZMQ_REP); 92 | zmq_bind (responder, "tcp://*:5555"); 93 | 94 | while (1) { 95 | // 等待客户端请求 96 | zmq_msg_t request; 97 | zmq_msg_init (&request); 98 | zmq_recv (responder, &request, 0); 99 | printf ("收到 Hello\n"); 100 | zmq_msg_close (&request); 101 | 102 | // 做些“处理” 103 | sleep (1); 104 | 105 | // 返回应答 106 | zmq_msg_t reply; 107 | zmq_msg_init_size (&reply, 5); 108 | memcpy (zmq_msg_data (&reply), "World", 5); 109 | zmq_send (responder, &reply, 0); 110 | zmq_msg_close (&reply); 111 | } 112 | // 程序不会运行到这里,以下只是演示我们应该如何结束 113 | zmq_close (responder); 114 | zmq_term (context); 115 | return 0; 116 | } 117 | ``` 118 | 119 | ```textdiagram 120 | +------------+ 121 | | | 122 | | 客户端 | 123 | | | 124 | +------------+ 125 | | REQ | 126 | \---+--------/ 127 | | ^ 128 | | | 129 | "Hello" "World" 130 | | | 131 | v | 132 | /--------+---\ 133 | | REP | 134 | +------------+ 135 | | | 136 | | 服务器 | 137 | | | 138 | +------------+ 139 | 140 | 141 | Figure # - Request-Reply 142 | ``` 143 | 144 | 使用REQ-REP套接字发送和接受消息是需要遵循一定规律的。客户端首先使用zmq_send()发送消息,再用zmq_recv()接收,如此循环。如果打乱了这个顺序(如连续发送两次)则会报错。类似地,服务端必须先进行接收,后进行发送。 145 | 146 | ZMQ使用C语言作为它参考手册的语言,本指南也以它作为示例程序的语言。如果你正在阅读本指南的在线版本,你可以看到示例代码的下方有其他语言的实现。如以下是C++语言: 147 | 148 | **hwserver.cpp: Hello World server** 149 | 150 | ```cpp 151 | // 152 | // Hello World 服务端 C++语言版 153 | // 绑定一个REP套接字至tcp://*:5555 154 | // 从客户端接收Hello,并应答World 155 | // 156 | #include 157 | #include 158 | #include 159 | #include 160 | 161 | int main () { 162 | // 准备上下文和套接字 163 | zmq::context_t context (1); 164 | zmq::socket_t socket (context, ZMQ_REP); 165 | socket.bind ("tcp://*:5555"); 166 | 167 | while (true) { 168 | zmq::message_t request; 169 | 170 | // 等待客户端请求 171 | socket.recv (&request); 172 | std::cout << "收到 Hello" << std::endl; 173 | 174 | // 做一些“处理” 175 | sleep (1); 176 | 177 | // 应答World 178 | zmq::message_t reply (5); 179 | memcpy ((void *) reply.data (), "World", 5); 180 | socket.send (reply); 181 | } 182 | return 0; 183 | } 184 | ``` 185 | 186 | 可以看到C语言和C++语言的API代码差不多,而在PHP这样的语言中,代码就会更为简洁: 187 | 188 | **hwserver.php: Hello World server** 189 | 190 | ```php 191 | 197 | */ 198 | 199 | $context = new ZMQContext(1); 200 | 201 | // 与客户端通信的套接字 202 | $responder = new ZMQSocket($context, ZMQ::SOCKET_REP); 203 | $responder->bind("tcp://*:5555"); 204 | 205 | while(true) { 206 | // 等待客户端请求 207 | $request = $responder->recv(); 208 | printf ("Received request: [%s]\n", $request); 209 | 210 | // 做一些“处理” 211 | sleep (1); 212 | 213 | // 应答World 214 | $responder->send("World"); 215 | } 216 | ``` 217 | 218 | 下面是客户端的代码: 219 | 220 | **hwclient: Hello World client in C** 221 | 222 | ```c 223 | // 224 | // Hello World 客户端 225 | // 连接REQ套接字至 tcp://localhost:5555 226 | // 发送Hello给服务端,并接收World 227 | // 228 | #include 229 | #include 230 | #include 231 | #include 232 | 233 | int main (void) 234 | { 235 | void *context = zmq_init (1); 236 | 237 | // 连接至服务端的套接字 238 | printf ("正在连接至hello world服务端...\n"); 239 | void *requester = zmq_socket (context, ZMQ_REQ); 240 | zmq_connect (requester, "tcp://localhost:5555"); 241 | 242 | int request_nbr; 243 | for (request_nbr = 0; request_nbr != 10; request_nbr++) { 244 | zmq_msg_t request; 245 | zmq_msg_init_size (&request, 5); 246 | memcpy (zmq_msg_data (&request), "Hello", 5); 247 | printf ("正在发送 Hello %d...\n", request_nbr); 248 | zmq_send (requester, &request, 0); 249 | zmq_msg_close (&request); 250 | 251 | zmq_msg_t reply; 252 | zmq_msg_init (&reply); 253 | zmq_recv (requester, &reply, 0); 254 | printf ("接收到 World %d\n", request_nbr); 255 | zmq_msg_close (&reply); 256 | } 257 | zmq_close (requester); 258 | zmq_term (context); 259 | return 0; 260 | } 261 | ``` 262 | 263 | 这看起来是否太简单了?ZMQ就是这样一个东西,你往里加点儿料就能制作出一枚无穷能量的原子弹,用它来拯救世界吧! 264 | 265 | ```textdiagram 266 | +------------+ +------------+ 267 | | | | | Zap! 268 | | TCP socket +------->| 0MQ socket | 269 | | | BOOM! | cC00 | POW!! 270 | +------------+ +------------+ 271 | ^ ^ ^ 272 | | | | 273 | | | +---------+ 274 | | | | 275 | | +----------+ | 276 | Illegal | | 277 | radioisotopes | | 278 | from secret | | 279 | Soviet atomic | Spandex 280 | city | 281 | Cosmic rays 282 | 283 | 284 | Figure # - A terrible accident... 285 | ``` 286 | 287 | 理论上你可以连接千万个客户端到这个服务端上,同时连接都没问题,程序仍会运作得很好。你可以尝试一下先打开客户端,再打开服务端,可以看到程序仍然会正常工作,想想这意味着什么。 288 | 289 | 让我简单介绍一下这两段程序到底做了什么。首先,他们创建了一个ZMQ上下文,然后是一个套接字。不要被这些陌生的名词吓到,后面我们都会讲到。服务端将REP套接字绑定到5555端口上,并开始等待请求,发出应答,如此循环。客户端则是发送请求并等待服务端的应答。 290 | 291 | 这些代码背后其实发生了很多很多事情,但是程序员完全不必理会这些,只要知道这些代码短小精悍,极少出错,耐高压。这种通信模式我们称之为请求-应答模式,是ZMQ最直接的一种应用。你可以拿它和RPC及经典的C/S模型做类比。 292 | 293 | ### 关于字符串 294 | 295 | ZMQ不会关心发送消息的内容,只要知道它所包含的字节数。所以,程序员需要做一些工作,保证对方节点能够正确读取这些消息。如何将一个对象或复杂数据类型转换成ZMQ可以发送的消息,这有类似Protocol Buffers的序列化软件可以做到。但对于字符串,你也是需要有所注意的。 296 | 297 | 在C语言中,字符串都以一个空字符结尾,你可以像这样发送一个完整的字符串: 298 | 299 | ```c 300 | zmq_msg_init_data (&request, "Hello", 6, NULL, NULL); 301 | ``` 302 | 303 | 但是,如果你用其他语言发送这个字符串,很可能不会包含这个空字节,如你使用Python发送: 304 | 305 | ```python 306 | socket.send ("Hello") 307 | ``` 308 | 309 | 实际发送的消息是: 310 | 311 | ```textdiagram 312 | +-----+ +-----+-----+-----+-----+-----+ 313 | | 5 | | H | e | l | l | o | 314 | +-----+ +-----+-----+-----+-----+-----+ 315 | 316 | 317 | Figure # - A 0MQ string 318 | ``` 319 | 320 | 如果你从C语言中读取该消息,你会读到一个类似于字符串的内容,甚至它可能就是一个字符串(第六位在内存中正好是一个空字符),但是这并不合适。这样一来,客户端和服务端对字符串的定义就不统一了,你会得到一些奇怪的结果。 321 | 322 | 当你用C语言从ZMQ中获取字符串,你不能够相信该字符串有一个正确的结尾。因此,当你在接受字符串时,应该建立多一个字节的缓冲区,将字符串放进去,并添加结尾。 323 | 324 | 所以,让我们做如下假设:**ZMQ的字符串是有长度的,且传送时不加结束符**。在最简单的情况下,ZMQ字符串和ZMQ消息中的一帧是等价的,就如上图所展现的,由一个长度属性和一串字节表示。 325 | 326 | 下面这个功能函数会帮助我们在C语言中正确的接受字符串消息: 327 | 328 | ```c 329 | // 从ZMQ套接字中接收字符串,并转换为C语言的字符串 330 | static char * 331 | s_recv (void *socket) { 332 | zmq_msg_t message; 333 | zmq_msg_init (&message); 334 | zmq_recv (socket, &message, 0); 335 | int size = zmq_msg_size (&message); 336 | char *string = malloc (size + 1); 337 | memcpy (string, zmq_msg_data (&message), size); 338 | zmq_msg_close (&message); 339 | string [size] = 0; 340 | return (string); 341 | } 342 | ``` 343 | 344 | 这段代码我们会在日后的示例中使用,我们可以顺手写一个s_send()方法,并打包成一个.h文件供我们使用。 345 | 346 | 这就诞生了zhelpers.h,一个供C语言使用的ZMQ功能函数库。它的源代码比较长,而且只对C语言程序员有用,你可以在闲暇时[看一看](https://github.com/imatix/zguide/blob/master/examples/C/zhelpers.h)。 347 | 348 | ### 获取版本号 349 | 350 | ZMQ目前有多个版本,而且仍在持续更新。如果你遇到了问题,也许这在下一个版本中已经解决了。想知道目前的ZMQ版本,你可以在程序中运行如下: 351 | 352 | **version: ØMQ version reporting in C** 353 | 354 | ```c 355 | // 356 | // 返回当前ZMQ的版本号 357 | // 358 | #include "zhelpers.h" 359 | 360 | int main (void) 361 | { 362 | int major, minor, patch; 363 | zmq_version (&major, &minor, &patch); 364 | printf ("当前ZMQ版本号为 %d.%d.%d\n", major, minor, patch); 365 | 366 | return EXIT_SUCCESS; 367 | } 368 | ``` 369 | 370 | ### 让消息流动起来 371 | 372 | 第二种经典的消息模式是单向数据分发:服务端将更新事件发送给一组客户端。让我们看一个天气信息发布的例子,包括邮编、温度、相对湿度。我们随机生成这些信息,气象站好像也是这么干的。 373 | 374 | 下面是服务端的代码,使用5556端口: 375 | 376 | **wuserver: Weather update server in C** 377 | 378 | ```c 379 | // 380 | // 气象信息更新服务 381 | // 绑定PUB套接字至tcp://*:5556端点 382 | // 发布随机气象信息 383 | // 384 | #include "zhelpers.h" 385 | 386 | int main (void) 387 | { 388 | // 准备上下文和PUB套接字 389 | void *context = zmq_init (1); 390 | void *publisher = zmq_socket (context, ZMQ_PUB); 391 | zmq_bind (publisher, "tcp://*:5556"); 392 | zmq_bind (publisher, "ipc://weather.ipc"); 393 | 394 | // 初始化随机数生成器 395 | srandom ((unsigned) time (NULL)); 396 | while (1) { 397 | // 生成数据 398 | int zipcode, temperature, relhumidity; 399 | zipcode = randof (100000); 400 | temperature = randof (215) - 80; 401 | relhumidity = randof (50) + 10; 402 | 403 | // 向所有订阅者发送消息 404 | char update [20]; 405 | sprintf (update, "%05d %d %d", zipcode, temperature, relhumidity); 406 | s_send (publisher, update); 407 | } 408 | zmq_close (publisher); 409 | zmq_term (context); 410 | return 0; 411 | } 412 | ``` 413 | 414 | 这项更新服务没有开始、没有结束,就像永不消失的电波一样。 415 | 416 | ```textdiagram 417 | +-------------+ 418 | | | 419 | | Publisher | 420 | | | 421 | +-------------+ 422 | | PUB | 423 | \-------------/ 424 | bind 425 | | 426 | | 427 | updates 428 | | 429 | +---------------+---------------+ 430 | | | | 431 | updates updates updates 432 | | | | 433 | | | | 434 | v v v 435 | connect connect connect 436 | /------------\ /------------\ /------------\ 437 | | SUB | | SUB | | SUB | 438 | +------------+ +------------+ +------------+ 439 | | | | | | | 440 | | Subscriber | | Subscriber | | Subscriber | 441 | | | | | | | 442 | +------------+ +------------+ +------------+ 443 | 444 | 445 | Figure # - Publish-Subscribe 446 | ``` 447 | 448 | 下面是客户端程序,它会接受发布者的消息,只处理特定邮编标注的信息,如纽约的邮编是10001: 449 | 450 | **wuclient: Weather update client in C** 451 | 452 | ```c 453 | // 454 | // 气象信息客户端 455 | // 连接SUB套接字至tcp://*:5556端点 456 | // 收集指定邮编的气象信息,并计算平均温度 457 | // 458 | #include "zhelpers.h" 459 | 460 | int main (int argc, char *argv []) 461 | { 462 | void *context = zmq_init (1); 463 | 464 | // 创建连接至服务端的套接字 465 | printf ("正在收集气象信息...\n"); 466 | void *subscriber = zmq_socket (context, ZMQ_SUB); 467 | zmq_connect (subscriber, "tcp://localhost:5556"); 468 | 469 | // 设置订阅信息,默认为纽约,邮编10001 470 | char *filter = (argc > 1)? argv [1]: "10001 "; 471 | zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, filter, strlen (filter)); 472 | 473 | // 处理100条更新信息 474 | int update_nbr; 475 | long total_temp = 0; 476 | for (update_nbr = 0; update_nbr < 100; update_nbr++) { 477 | char *string = s_recv (subscriber); 478 | int zipcode, temperature, relhumidity; 479 | sscanf (string, "%d %d %d", 480 | &zipcode, &temperature, &relhumidity); 481 | total_temp += temperature; 482 | free (string); 483 | } 484 | printf ("地区邮编 '%s' 的平均温度为 %dF\n", 485 | filter, (int) (total_temp / update_nbr)); 486 | 487 | zmq_close (subscriber); 488 | zmq_term (context); 489 | return 0; 490 | } 491 | ``` 492 | 493 | 需要注意的是,在使用SUB套接字时,必须使用zmq_setsockopt()方法来设置订阅的内容。如果你不设置订阅内容,那将什么消息都收不到,新手很容易犯这个错误。订阅信息可以是任何字符串,可以设置多次。只要消息满足其中一条订阅信息,SUB套接字就会收到。订阅者可以选择不接收某类消息,也是通过zmq_setsockopt()方法实现的。 494 | 495 | PUB-SUB套接字组合是异步的。客户端在一个循环体中使用zmq_recv()接收消息,如果向SUB套接字发送消息则会报错;类似地,服务端可以不断地使用zmq_send()发送消息,但不能再PUB套接字上使用zmq_recv()。 496 | 497 | 关于PUB-SUB套接字,还有一点需要注意:你无法得知SUB是何时开始接收消息的。就算你先打开了SUB套接字,后打开PUB发送消息,这时SUB还是会丢失一些消息的,因为建立连接是需要一些时间的。很少,但并不是零。 498 | 499 | 这种“慢连接”的症状一开始会让很多人困惑,所以这里我要详细解释一下。还记得ZMQ是在后台进行异步的I/O传输的,如果你有两个节点用以下顺序相连: 500 | 501 | * 订阅者连接至端点接收消息并计数; 502 | * 发布者绑定至端点并立刻发送1000条消息。 503 | 504 | 运行的结果很可能是订阅者一条消息都收不到。这时你可能会傻眼,忙于检查有没有设置订阅信息,并重新尝试,但结果还是一样。 505 | 506 | 我们知道在建立TCP连接时需要进行三次握手,会耗费几毫秒的时间,而当节点数增加时这个数字也会上升。在这么短的时间里,ZMQ就可以发送很多很多消息了。举例来说,如果建立连接需要耗时5毫秒,而ZMQ只需要1毫秒就可以发送完这1000条消息。 507 | 508 | 第二章中我会解释如何使发布者和订阅者同步,只有当订阅者准备好时发布者才会开始发送消息。有一种简单的方法来同步PUB和SUB,就是让PUB延迟一段时间再发送消息。现实编程中我不建议使用这种方式,因为它太脆弱了,而且不好控制。不过这里我们先暂且使用sleep的方式来解决,等到第二章的时候再讲述正确的处理方式。 509 | 510 | 另一种同步的方式则是认为发布者的消息流是无穷无尽的,因此丢失了前面一部分信息也没有关系。我们的气象信息客户端就是这么做的。 511 | 512 | 示例中的气象信息客户端会收集指定邮编的一千条信息,其间大约有1000万条信息被发布。你可以先打开客户端,再打开服务端,工作一段时间后重启服务端,这时客户端仍会正常工作。当客户端收集完所需信息后,会计算并输出平均温度。 513 | 514 | 关于发布-订阅模式的几点说明: 515 | 516 | * 订阅者可以连接多个发布者,轮流接收消息; 517 | * 如果发布者没有订阅者与之相连,那它发送的消息将直接被丢弃; 518 | * 如果你使用TCP协议,那当订阅者处理速度过慢时,消息会在发布者处堆积。以后我们会讨论如何使用阈值(HWM)来保护发布者。 519 | * 在目前版本的ZMQ中,消息的过滤是在订阅者处进行的。也就是说,发布者会向订阅者发送所有的消息,订阅者会将未订阅的消息丢弃。 520 | 521 | 我在自己的四核计算机上尝试发布1000万条消息,速度很快,但没什么特别的: 522 | 523 | ``` 524 | ph@ws200901:~/work/git/0MQGuide/examples/c$ time wuclient 525 | Collecting updates from weather server... 526 | Average temperature for zipcode '10001 ' was 18F 527 | 528 | real 0m5.939s 529 | user 0m1.590s 530 | sys 0m2.290s 531 | ``` 532 | 533 | ### 分布式处理 534 | 535 | 下面一个示例程序中,我们将使用ZMQ进行超级计算,也就是并行处理模型: 536 | 537 | * 任务分发器会生成大量可以并行计算的任务; 538 | * 有一组worker会处理这些任务; 539 | * 结果收集器会在末端接收所有worker的处理结果,进行汇总。 540 | 541 | 现实中,worker可能散落在不同的计算机中,利用GPU(图像处理单元)进行复杂计算。下面是任务分发器的代码,它会生成100个任务,任务内容是让收到的worker延迟若干毫秒。 542 | 543 | **taskvent: Parallel task ventilator in C** 544 | 545 | ```c 546 | // 547 | // 任务分发器 548 | // 绑定PUSH套接字至tcp://localhost:5557端点 549 | // 发送一组任务给已建立连接的worker 550 | // 551 | #include "zhelpers.h" 552 | 553 | int main (void) 554 | { 555 | void *context = zmq_init (1); 556 | 557 | // 用于发送消息的套接字 558 | void *sender = zmq_socket (context, ZMQ_PUSH); 559 | zmq_bind (sender, "tcp://*:5557"); 560 | 561 | // 用于发送开始信号的套接字 562 | void *sink = zmq_socket (context, ZMQ_PUSH); 563 | zmq_connect (sink, "tcp://localhost:5558"); 564 | 565 | printf ("准备好worker后按任意键开始: "); 566 | getchar (); 567 | printf ("正在向worker分配任务...\n"); 568 | 569 | // 发送开始信号 570 | s_send (sink, "0"); 571 | 572 | // 初始化随机数生成器 573 | srandom ((unsigned) time (NULL)); 574 | 575 | // 发送100个任务 576 | int task_nbr; 577 | int total_msec = 0; // 预计执行时间(毫秒) 578 | for (task_nbr = 0; task_nbr < 100; task_nbr++) { 579 | int workload; 580 | // 随机产生1-100毫秒的工作量 581 | workload = randof (100) + 1; 582 | total_msec += workload; 583 | char string [10]; 584 | sprintf (string, "%d", workload); 585 | s_send (sender, string); 586 | } 587 | printf ("预计执行时间: %d 毫秒\n", total_msec); 588 | sleep (1); // 延迟一段时间,让任务分发完成 589 | 590 | zmq_close (sink); 591 | zmq_close (sender); 592 | zmq_term (context); 593 | return 0; 594 | } 595 | ``` 596 | 597 | ```textdiagram 598 | +-------------+ 599 | | | 600 | | Ventilator | 601 | | | 602 | +-------------+ 603 | | PUSH | 604 | \------+------/ 605 | | 606 | tasks 607 | | 608 | +---------------+---------------+ 609 | | | | 610 | task task task 611 | | | | 612 | v v v 613 | /------------\ /------------\ /------------\ 614 | | PULL | | PULL | | PULL | 615 | +------------+ +------------+ +------------+ 616 | | | | | | | 617 | | Worker | | Worker | | Worker | 618 | | | | | | | 619 | +------------+ +------------+ +------------+ 620 | | PUSH | | PUSH | | PUSH | 621 | \-----+------/ \-----+------/ \-----+------/ 622 | | | | 623 | result result result 624 | | | | 625 | +---------------+---------------+ 626 | | 627 | results 628 | | 629 | v 630 | /-------------\ 631 | | PULL | 632 | +-------------+ 633 | | | 634 | | Sink | 635 | | | 636 | +-------------+ 637 | 638 | 639 | Figure # - Parallel Pipeline 640 | ``` 641 | 642 | 下面是worker的代码,它接受信息并延迟指定的毫秒数,并发送执行完毕的信号: 643 | 644 | **taskwork: Parallel task worker in C** 645 | 646 | ```c 647 | // 648 | // 任务执行器 649 | // 连接PULL套接字至tcp://localhost:5557端点 650 | // 从任务分发器处获取任务 651 | // 连接PUSH套接字至tcp://localhost:5558端点 652 | // 向结果采集器发送结果 653 | // 654 | #include "zhelpers.h" 655 | 656 | int main (void) 657 | { 658 | void *context = zmq_init (1); 659 | 660 | // 获取任务的套接字 661 | void *receiver = zmq_socket (context, ZMQ_PULL); 662 | zmq_connect (receiver, "tcp://localhost:5557"); 663 | 664 | // 发送结果的套接字 665 | void *sender = zmq_socket (context, ZMQ_PUSH); 666 | zmq_connect (sender, "tcp://localhost:5558"); 667 | 668 | // 循环处理任务 669 | while (1) { 670 | char *string = s_recv (receiver); 671 | // 输出处理进度 672 | fflush (stdout); 673 | printf ("%s.", string); 674 | 675 | // 开始处理 676 | s_sleep (atoi (string)); 677 | free (string); 678 | 679 | // 发送结果 680 | s_send (sender, ""); 681 | } 682 | zmq_close (receiver); 683 | zmq_close (sender); 684 | zmq_term (context); 685 | return 0; 686 | } 687 | ``` 688 | 689 | 下面是结果收集器的代码。它会收集100个处理结果,并计算总的执行时间,让我们由此判别任务是否是并行计算的。 690 | 691 | **tasksink: Parallel task sink in C** 692 | 693 | ```c 694 | // 695 | // 任务收集器 696 | // 绑定PULL套接字至tcp://localhost:5558端点 697 | // 从worker处收集处理结果 698 | // 699 | #include "zhelpers.h" 700 | 701 | int main (void) 702 | { 703 | // 准备上下文和套接字 704 | void *context = zmq_init (1); 705 | void *receiver = zmq_socket (context, ZMQ_PULL); 706 | zmq_bind (receiver, "tcp://*:5558"); 707 | 708 | // 等待开始信号 709 | char *string = s_recv (receiver); 710 | free (string); 711 | 712 | // 开始计时 713 | int64_t start_time = s_clock (); 714 | 715 | // 确定100个任务均已处理 716 | int task_nbr; 717 | for (task_nbr = 0; task_nbr < 100; task_nbr++) { 718 | char *string = s_recv (receiver); 719 | free (string); 720 | if ((task_nbr / 10) * 10 == task_nbr) 721 | printf (":"); 722 | else 723 | printf ("."); 724 | fflush (stdout); 725 | } 726 | // 计算并输出总执行时间 727 | printf ("执行时间: %d 毫秒\n", 728 | (int) (s_clock () - start_time)); 729 | 730 | zmq_close (receiver); 731 | zmq_term (context); 732 | return 0; 733 | } 734 | ``` 735 | 736 | 一组任务的平均执行时间在5秒左右,以下是分别开始1个、2个、4个worker时的执行结果: 737 | 738 | ``` 739 | # 1 worker 740 | Total elapsed time: 5034 msec 741 | # 2 workers 742 | Total elapsed time: 2421 msec 743 | # 4 workers 744 | Total elapsed time: 1018 msec 745 | ``` 746 | 747 | 关于这段代码的几个细节: 748 | 749 | * worker上游和任务分发器相连,下游和结果收集器相连,这就意味着你可以开启任意多个worker。但若worker是绑定至端点的,而非连接至端点,那我们就需要准备更多的端点,并配置任务分发器和结果收集器。所以说,任务分发器和结果收集器是这个网络结构中较为稳定的部分,因此应该由它们绑定至端点,而非worker,因为它们较为动态。 750 | 751 | * 我们需要做一些同步的工作,等待worker全部启动之后再分发任务。这点在ZMQ中很重要,且不易解决。连接套接字的动作会耗费一定的时间,因此当第一个worker连接成功时,它会一下收到很多任务。所以说,如果我们不进行同步,那这些任务根本就不会被并行地执行。你可以自己试验一下。 752 | 753 | * 任务分发器使用PUSH套接字向worker均匀地分发任务(假设所有的worker都已经连接上了),这种机制称为_负载均衡_,以后我们会见得更多。 754 | 755 | * 结果收集器的PULL套接字会均匀地从worker处收集消息,这种机制称为_公平队列_: 756 | 757 | ```textdiagram 758 | +---------+ +---------+ +---------+ 759 | | PUSH | | PUSH | | PUSH | 760 | \----+----/ \----+----/ \----+----/ 761 | | | | 762 | R1, R2, R3 R4 R5, R6 763 | | | | 764 | +-------------+-------------+ 765 | | 766 | fair-queuing 767 | R1, R4, R5, R2, R6, R3 768 | | 769 | v 770 | /-------------\ 771 | | PULL | 772 | +-------------+ 773 | 774 | 775 | Figure # - Fair queuing 776 | ``` 777 | 778 | 管道模式也会出现慢连接的情况,让人误以为PUSH套接字没有进行负载均衡。如果你的程序中某个worker接收到了更多的请求,那是因为它的PULL套接字连接得比较快,从而在别的worker连接之前获取了额外的消息。 779 | 780 | ### 使用ZMQ编程 781 | 782 | 看着这些示例程序后,你一定迫不及待想要用ZMQ进行编程了。不过在开始之前,我还有几条建议想给到你,这样可以省去未来的一些麻烦: 783 | 784 | * 学习ZMQ要循序渐进,虽然它只是一套API,但却提供了无尽的可能。一步一步学习它提供的功能,并完全掌握。 785 | 786 | * 编写漂亮的代码。丑陋的代码会隐藏问题,让想要帮助你的人无从下手。比如,你会习惯于使用无意义的变量名,但读你代码的人并不知道。应使用有意义的变量名称,而不是随意起一个。代码的缩进要统一,布局清晰。漂亮的代码可以让你的世界变得更美好。 787 | 788 | * 边写边测试,当代码出现问题,你就可以快速定位到某些行。这一点在编写ZMQ应用程序时尤为重要,因为很多时候你无法第一次就编写出正确的代码。 789 | 790 | * 当你发现自己编写的代码无法正常工作时,你可以将其拆分成一些代码片段,看看哪段没有正确地执行。ZMQ可以让你构建非常模块化的代码,所以应该好好利用这一点。 791 | 792 | * 需要时应使用抽象的方法来编写程序(类、成员函数等等),不要随意拷贝代码,因为拷贝代码的同时也是在拷贝错误。 793 | 794 | 我们看看下面这段代码,是某位同仁让我帮忙修改的: 795 | 796 | ```c 797 | // 注意:不要使用这段代码! 798 | static char *topic_str = "msg.x|"; 799 | 800 | void* pub_worker(void* arg){ 801 | void *ctx = arg; 802 | assert(ctx); 803 | 804 | void *qskt = zmq_socket(ctx, ZMQ_REP); 805 | assert(qskt); 806 | 807 | int rc = zmq_connect(qskt, "inproc://querys"); 808 | assert(rc == 0); 809 | 810 | void *pubskt = zmq_socket(ctx, ZMQ_PUB); 811 | assert(pubskt); 812 | 813 | rc = zmq_bind(pubskt, "inproc://publish"); 814 | assert(rc == 0); 815 | 816 | uint8_t cmd; 817 | uint32_t nb; 818 | zmq_msg_t topic_msg, cmd_msg, nb_msg, resp_msg; 819 | 820 | zmq_msg_init_data(&topic_msg, topic_str, strlen(topic_str) , NULL, NULL); 821 | 822 | fprintf(stdout,"WORKER: ready to recieve messages\n"); 823 | // 注意:不要使用这段代码,它不能工作! 824 | // e.g. topic_msg will be invalid the second time through 825 | while (1){ 826 | zmq_send(pubskt, &topic_msg, ZMQ_SNDMORE); 827 | 828 | zmq_msg_init(&cmd_msg); 829 | zmq_recv(qskt, &cmd_msg, 0); 830 | memcpy(&cmd, zmq_msg_data(&cmd_msg), sizeof(uint8_t)); 831 | zmq_send(pubskt, &cmd_msg, ZMQ_SNDMORE); 832 | zmq_msg_close(&cmd_msg); 833 | 834 | fprintf(stdout, "recieved cmd %u\n", cmd); 835 | 836 | zmq_msg_init(&nb_msg); 837 | zmq_recv(qskt, &nb_msg, 0); 838 | memcpy(&nb, zmq_msg_data(&nb_msg), sizeof(uint32_t)); 839 | zmq_send(pubskt, &nb_msg, 0); 840 | zmq_msg_close(&nb_msg); 841 | 842 | fprintf(stdout, "recieved nb %u\n", nb); 843 | 844 | zmq_msg_init_size(&resp_msg, sizeof(uint8_t)); 845 | memset(zmq_msg_data(&resp_msg), 0, sizeof(uint8_t)); 846 | zmq_send(qskt, &resp_msg, 0); 847 | zmq_msg_close(&resp_msg); 848 | 849 | } 850 | return NULL; 851 | } 852 | ``` 853 | 854 | 下面是我为他重写的代码,顺便修复了一些BUG: 855 | 856 | ```c 857 | static void * 858 | worker_thread (void *arg) { 859 | void *context = arg; 860 | void *worker = zmq_socket (context, ZMQ_REP); 861 | assert (worker); 862 | int rc; 863 | rc = zmq_connect (worker, "ipc://worker"); 864 | assert (rc == 0); 865 | 866 | void *broadcast = zmq_socket (context, ZMQ_PUB); 867 | assert (broadcast); 868 | rc = zmq_bind (broadcast, "ipc://publish"); 869 | assert (rc == 0); 870 | 871 | while (1) { 872 | char *part1 = s_recv (worker); 873 | char *part2 = s_recv (worker); 874 | printf ("Worker got [%s][%s]\n", part1, part2); 875 | s_sendmore (broadcast, "msg"); 876 | s_sendmore (broadcast, part1); 877 | s_send (broadcast, part2); 878 | free (part1); 879 | free (part2); 880 | 881 | s_send (worker, "OK"); 882 | } 883 | return NULL; 884 | } 885 | ``` 886 | 887 | 上段程序的最后,它将套接字在两个线程之间传递,这会导致莫名其妙的问题。这种行为在ZMQ 2.1中虽然是合法的,但是不提倡使用。 888 | 889 | ### ZMQ 2.1版 890 | 891 | 历史告诉我们,ZMQ 2.0是一个低延迟的分布式消息系统,它从众多同类软件中脱颖而出,摆脱了各种奢华的名目,向世界宣告“无极限”的口号。这是我们一直在使用的稳定发行版。 892 | 893 | 时过境迁,2010年流行的东西在2011年就不一定了。当ZMQ的开发者和社区开发者在激烈地讨论ZMQ的种种问题时,ZMQ 2.1横空出世了,成为新的稳定发行版。 894 | 895 | 本指南主要针对ZMQ 2.1进行描述,因此对于从ZMQ 2.0迁移过来的开发者来说有一些需要注意的地方: 896 | 897 | * 在2.0中,调用zmq_close()和zmq_term()时会丢弃所有尚未发送的消息,所以在发送完消息后不能直接关闭程序,2.0的示例中往往使用sleep(1)来规避这个问题。但是在2.1中就不需要这样做了,程序会等待消息全部发送完毕后再退出。 898 | 899 | * 相反地,2.0中可以在尚有套接字打开的情况下调用zmq_term(),这在2.1中会变得不安全,会造成程序的阻塞。所以,在2.1程序中我们_会先关闭所有的套接字_,然后才退出程序。如果套接字中有尚未发送的消息,程序就会一直处于等待状态,_除非手工设置了套接字的LINGER选项_(如设置为零),那么套接字会在相应的时间后关闭。 900 | 901 | ```c 902 | int zero = 0; 903 | zmq_setsockopt (mysocket, ZMQ_LINGER, &zero, sizeof (zero)); 904 | ``` 905 | 906 | * 2.0中,zmq_poll()函数没有定时功能,它会在满足条件时立刻返回,我们需要在循环体中检查还有多少剩余。但在2.1中,zmq_poll()会在指定时间后返回,因此可以作为定时器使用。 907 | 908 | * 2.0中,ZMQ会忽略系统的中断消息,这就意味着对libzmq的调用是不会收到EINTR消息的,这样就无法对SIGINT(Ctrl-C)等消息进行处理了。在2.1中,这个问题得以解决,像类似zmq_recv()的方法都会接收并返回系统的EINTR消息。 909 | 910 | ### 正确地使用上下文 911 | 912 | ZMQ应用程序的一开始总是会先创建一个上下文,并用它来创建套接字。在C语言中,创建上下文的函数是zmq_init()。一个进程中只应该创建一个上下文。从技术的角度来说,上下文是一个容器,包含了该进程下所有的套接字,并为inproc协议提供实现,用以高速连接进程内不同的线程。如果一个进程中创建了两个上下文,那就相当于启动了两个ZMQ实例。如果这正是你需要的,那没有问题,但一般情况下: 913 | 914 | **在一个进程中使用zmq_init()函数创建一个上下文,并在结束时使用zmq_term()函数关闭它** 915 | 916 | 如果你使用了fork()系统调用,那每个进程需要自己的上下文对象。如果在调用fork()之前调用了zmq_init()函数,那每个子进程都会有自己的上下文对象。通常情况下,你会需要在子进程中做些有趣的事,而让父进程来管理它们。 917 | 918 | ### 正确地退出和清理 919 | 920 | 程序员的一个良好习惯是:总是在结束时进行清理工作。当你使用像Python那样的语言编写ZMQ应用程序时,系统会自动帮你完成清理。但如果使用的是C语言,那就需要小心地处理了,否则可能发生内存泄露、应用程序不稳定等问题。 921 | 922 | 内存泄露只是问题之一,其实ZMQ是很在意程序的退出方式的。个中原因比较复杂,但简单的来说,如果仍有套接字处于打开状态,调用zmq_term()时会导致程序挂起;就算关闭了所有的套接字,如果仍有消息处于待发送状态,zmq_term()也会造成程序的等待。只有当套接字的LINGER选项设为0时才能避免。 923 | 924 | 我们需要关注的ZMQ对象包括:消息、套接字、上下文。好在内容并不多,至少在一般的应用程序中是这样: 925 | 926 | * 处理完消息后,记得用zmq_msg_close()函数关闭消息; 927 | * 如果你同时打开或关闭了很多套接字,那可能需要重新规划一下程序的结构了; 928 | * 退出程序时,应该先关闭所有的套接字,最后调用zmq_term()函数,销毁上下文对象。 929 | 930 | 如果要用ZMQ进行多线程的编程,需要考虑的问题就更多了。我们会在下一章中详述多线程编程,但如果你耐不住性子想要尝试一下,以下是在退出时的一些建议: 931 | 932 | * 不要在多个线程中使用同一个套接字。不要去想为什么,反正别这么干就是了。 933 | * 关闭所有的套接字,并在主程序中关闭上下文对象。 934 | * 如果仍有处于阻塞状态的recv或poll调用,应该在主程序中捕捉这些错误,并在相应的线程中关闭套接字。不要重复关闭上下文,zmq_term()函数会等待所有的套接字安全地关闭后才结束。 935 | 936 | 看吧,过程是复杂的,所以不同语言的API实现者可能会将这些步骤封装起来,让结束程序变得不那么复杂。 937 | 938 | ### 我们为什么需要ZMQ 939 | 940 | 现在我们已经将ZMQ运行起来了,让我们回顾一下为什么我们需要ZMQ: 941 | 942 | 目前的应用程序很多都会包含跨网络的组件,无论是局域网还是因特网。这些程序的开发者都会用到某种消息通信机制。有些人会使用某种消息队列产品,而大多数人则会自己手工来做这些事,使用TCP或UDP协议。这些协议使用起来并不困难,但是,简单地将消息从A发给B,和在任何情况下都能进行可靠的消息传输,这两种情况显然是不同的。 943 | 944 | 让我们看看在使用纯TCP协议进行消息传输时会遇到的一些典型问题。任何可复用的消息传输层肯定或多或少地会要解决以下问题: 945 | 946 | * 如何处理I/O?是让程序阻塞等待响应,还是在后台处理这些事?这是软件设计的关键因素。阻塞式的I/O操作会让程序架构难以扩展,而后台处理I/O也是比较困难的。 947 | 948 | * 如何处理那些临时的、来去自由的组件?我们是否要将组件分为客户端和服务端两种,并要求服务端永不消失?那如果我们想要将服务端相连怎么办?我们要每隔几秒就进行重连吗? 949 | 950 | * 我们如何表示一条消息?我们怎样通过拆分消息,让其变得易读易写,不用担心缓存溢出,既能高效地传输小消息,又能胜任视频等大型文件的传输? 951 | 952 | * 如何处理那些不能立刻发送出去的消息?比如我们需要等待一个网络组件重新连接的时候?我们是直接丢弃该条消息,还是将它存入数据库,或是内存中的一个队列? 953 | 954 | * 要在哪里保存消息队列?如果某个组件读取消息队列的速度很慢,造成消息的堆积怎么办?我们要采取什么样的策略? 955 | 956 | * 如何处理丢失的消息?我们是等待新的数据,请求重发,还是需要建立一套新的可靠性机制以保证消息不会丢失?如果这个机制自身崩溃了呢? 957 | 958 | * 如果我们想换一种网络连接协议,如用广播代替TCP单播?或者改用IPv6?我们是否需要重写所有的应用程序,或者将这种协议抽象到一个单独的层中? 959 | 960 | * 我们如何对消息进行路由?我们可以将消息同时发送给多个节点吗?是否能将应答消息返回给请求的发送方? 961 | 962 | * 我们如何为另一种语言写一个API?我们是否需要完全重写某项协议,还是重新打包一个类库? 963 | 964 | * 怎样才能做到在不同的架构之间传送消息?是否需要为消息规定一种编码? 965 | 966 | * 我们如何处理网络通信错误?等待并重试,还是直接忽略或取消? 967 | 968 | 我们可以找一个开源软件来做例子,如[Hadoop Zookeeper](http://hadoop.apache.org/zookeeper/),看一下它的C语言API源码,[src/c/src/zookeeper.c]([http://github.com/apache/zookeeper/blob/trunk/src/c/src/zookeeper.c src/c/src/zookeeper.c)。这段代码大约有3200行,没有注释,实现了一个C/S网络通信协议。它工作起来很高效,因为使用了poll()来代替select()。但是,Zookeeper应该被抽象出来,作为一种通用的消息通信层,并加以详细的注释。像这样的模块应该得到最大程度上的复用,而不是重复地制造轮子。 969 | 970 | ```textdiagram 971 | +------------+ 972 | | | 973 | | Piece A | 974 | | | 975 | +------------+ 976 | ^ 977 | | 978 | TCP 979 | | 980 | v 981 | +------------+ 982 | | | 983 | | Piece B | 984 | | | 985 | +------------+ 986 | 987 | 988 | Figure # - Messaging as it starts 989 | ``` 990 | 991 | 但是,如何编写这样一个可复用的消息层呢?为什么长久以来人们宁愿在自己的代码中重复书写控制原始TCP套接字的代码,而不愿编写这样一个公共库呢? 992 | 993 | 其实,要编写一个通用的消息层是件非常困难的事,这也是为什么FOSS项目不断在尝试,一些商业化的消息产品如此之复杂、昂贵、僵硬、脆弱。2006年,iMatix设计了AMQP协议,为FOSS项目的开发者提供了可能是当时第一个可复用的消息系统。[AMQP][]比其他同类产品要来得好,但[仍然是复杂、昂贵和脆弱的](http://www.imatix.com/articles:whats-wrong-with-amqp)。它需要花费几周的时间去学习,花费数月的时间去创建一个真正能用的架构,到那时可能为时已晚了。 994 | 995 | 大多数消息系统项目,如AMQP,为了解决上面提到的种种问题,发明了一些新的概念,如“代理”的概念,将寻址、路由、队列等功能都包含了进来。结果就是在一个没有任何注释的协议之上,又构建了一个C/S协议和相应的API,让应用程序和代理相互通信。代理的确是一个不错的解决方案,帮助降低大型网络结构的复杂度。但是,在Zookeeper这样的项目中应用代理机制的消息系统,可能是件更加糟糕的事,因为这意味了需要添加一台新的计算机,并构成一个新的单点故障。代理会逐渐成为新的瓶颈,管理起来更具风险。如果软件支持,我们可以添加第二个、第三个、第四个代理,构成某种冗余容错的模式。有人就是这么做的,这让系统架构变得更为复杂,增加了隐患。 996 | 997 | 在这种以代理为中心的架构下,需要一支专门的运维团队。你需要昼夜不停地观察代理的状态,不时地用棍棒调教他们。你需要添加计算机,以及更多的备份机,你需要有专人管理这些机器。这样做只对那些大型的网络应用程序才有意义,因为他们有更多可移动的模块,有多个团队进行开发和维护,而且已经经过了多年的建设。 998 | 999 | 这样一来,中小应用程序的开发者们就无计可施了。他们只能设法避免编写网络应用程序,转而编写那些不需要扩展的程序;或者可以使用原始的方式进行网络编程,但编写的软件会非常脆弱和复杂,难以维护;亦或者他们选择一种消息通信产品,虽然能够开发出扩展性强的应用程序,但需要支付高昂的代价。似乎没有一种选择是合理的,这也是为什么在上个世纪消息系统会成为一个广泛的问题。 1000 | 1001 | ```textdiagram 1002 | +---+ | +---+ 1003 | +---+ | | +---+ | | | 1004 | | +-->| | | | | | | 1005 | | | +---+ | | | +-+-+ 1006 | +-+-+ +-+-+ | | 1007 | | | | | 1008 | | +-----------------+ 1009 | | | | | 1010 | +-----------------------+ 1011 | | | | | 1012 | +-------|-------|----+--|------+ 1013 | | v | v | 1014 | +-+-+ +---+ | +---+ | 1015 | | | | | +-+-+ | | | 1016 | | | | | | | | | | 1017 | +---+ +---+ | | +---+ | 1018 | ^ +---+ ^ | 1019 | | ^ | +-+ 1020 | +-------+-------+-------+ | 1021 | | | | | 1022 | v +-+-+ v +---+ | 1023 | +---+ | | +---+ | | | 1024 | | | | |<--+ | | |<-+ 1025 | | | +---+ | | +-+-+ 1026 | +---+ +---+ 1027 | 1028 | 1029 | Figure # - Messaging as it becomes 1030 | ``` 1031 | 1032 | 我们真正需要的是这样一种消息软件,它能够做大型消息软件所能做的一切,但使用起来又非常简单,成本很低,可以用到所有的应用程序中,没有任何依赖条件。因为没有了额外的模块,就降低了出错的概率。这种软件需要能够在所有的操作系统上运行,并能支持所有的编程语言。 1033 | 1034 | ZMQ就是这样一种软件:它高效,提供了嵌入式的类库,使应用程序能够很好地在网络中扩展,成本低廉。 1035 | 1036 | ZMQ的主要特点有: 1037 | 1038 | * ZMQ会在后台线程异步地处理I/O操作,它使用一种不会死锁的数据结构来存储消息。 1039 | * 网络组件可以来去自如,ZMQ会负责自动重连,这就意味着你可以以任何顺序启动组件;用它创建的面向服务架构(SOA)中,服务端可以随意地加入或退出网络。 1040 | * ZMQ会在有必要的情况下自动将消息放入队列中保存,一旦建立了连接就开始发送。 1041 | * ZMQ有阈值(HWM)的机制,可以避免消息溢出。当队列已满,ZMQ会自动阻塞发送者,或丢弃部分消息,这些行为取决于你所使用的消息模式。 1042 | * ZMQ可以让你用不同的通信协议进行连接,如TCP、广播、进程内、进程间。改变通信协议时你不需要去修改代码。 1043 | * ZMQ会恰当地处理速度较慢的节点,会根据消息模式使用不同的策略。 1044 | * ZMQ提供了多种模式进行消息路由,如请求-应答模式、发布-订阅模式等。这些模式可以用来搭建网络拓扑结构。 1045 | * ZMQ中可以根据消息模式建立起一些中间装置(很小巧),可以用来降低网络的复杂程度。 1046 | * ZMQ会发送整个消息,使用消息帧的机制来传递。如果你发送了10KB大小的消息,你就会收到10KB大小的消息。 1047 | * ZMQ不强制使用某种消息格式,消息可以是0字节的,或是大到GB级的数据。当你表示这些消息时,可以选用诸如谷歌的protocol buffers,XDR等序列化产品。 1048 | * ZMQ能够智能地处理网络错误,有时它会进行重试,有时会告知你某项操作发生了错误。 1049 | * ZMQ甚至可以降低对环境的污染,因为节省了CPU时间意味着节省了电能。 1050 | 1051 | 其实ZMQ可以做的还不止这些,它会颠覆人们编写网络应用程序的模式。虽然从表面上看,它不过是提供了一套处理套接字的API,能够用zmq_recv()和zmq_send()进行消息的收发,但是,消息处理将成为应用程序的核心部分,很快你的程序就会变成一个个消息处理模块,这既美观又自然。它的扩展性还很强,每项任务由一个节点(节点是一个线程)、同一台机器上的两个节点(节点是一个进程)、同一网络上的两台机器(节点是一台机器)来处理,而不需要改动应用程序。 1052 | 1053 | ### 套接字的扩展性 1054 | 1055 | 我们来用实例看看ZMQ套接字的扩展性。这个脚本会启动气象信息服务及多个客户端: 1056 | 1057 | ``` 1058 | wuserver & 1059 | wuclient 12345 & 1060 | wuclient 23456 & 1061 | wuclient 34567 & 1062 | wuclient 45678 & 1063 | wuclient 56789 & 1064 | ``` 1065 | 1066 | 执行过程中,我们可以通过top命令查看进程状态(以下是一台四核机器的情况): 1067 | 1068 | ``` 1069 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1070 | 7136 ph 20 0 1040m 959m 1156 R 157 12.0 16:25.47 wuserver 1071 | 7966 ph 20 0 98608 1804 1372 S 33 0.0 0:03.94 wuclient 1072 | 7963 ph 20 0 33116 1748 1372 S 14 0.0 0:00.76 wuclient 1073 | 7965 ph 20 0 33116 1784 1372 S 6 0.0 0:00.47 wuclient 1074 | 7964 ph 20 0 33116 1788 1372 S 5 0.0 0:00.25 wuclient 1075 | 7967 ph 20 0 33072 1740 1372 S 5 0.0 0:00.35 wuclient 1076 | ``` 1077 | 1078 | 我们想想现在发生了什么:气象信息服务程序有一个单独的套接字,却能同时向五个客户端并行地发送消息。我们可以有成百上千个客户端并行地运作,服务端看不到这些客户端,不能操纵它们。 1079 | 1080 | ### 如果解决丢失消息的问题 1081 | 1082 | 在编写ZMQ应用程序时,你遇到最多的问题可能是无法获得消息。下面有一个问题解决路线图,列举了最基本的出错原因。不用担心其中的某些术语你没有见过,在后面的几章里都会讲到。 1083 | 1084 | ```textdiagram 1085 | +-----------------+ 1086 | | | 1087 | | 我获取不到数据! | 1088 | | | 1089 | | {o} | 1090 | +--------+--------+ 1091 | | 1092 | | 1093 | v 1094 | +-----------------+ +-----------------+ +------------------+ 1095 | | | | | | 使用函数 | 1096 | | 你是在从SUB套接 | | 有没有设置消息 | | zmq_setsockopt() | 1097 | | 字获取消息吗? +------->| 订阅信息? +------->| 设置 | 1098 | | | Yes | | No | ZMQ_SUBSCRIBE | 1099 | | {o} | | {o} | | 为空字符串 | 1100 | +--------+--------+ +--------+--------+ +------------------+ 1101 | | No | Yes 1102 | | | 1103 | | v 1104 | | +-----------------+ +------------------+ 1105 | | | | | 先打开所有SUB套接| 1106 | | | 是否在PUB之后 | | 字,再打开PUB,以| 1107 | | | 启动SUB? +------->| 避免消息丢失。 | 1108 | | | | No | | 1109 | | | {o} | | | 1110 | | +--------+--------+ +------------------+ 1111 | | | Yes 1112 | | | 1113 | | v 1114 | | +-------------------------+ 1115 | | | 请查看关于"慢链接"的 | 1116 | | | 解释 | 1117 | | | | 1118 | | +-------------------------+ 1119 | | 1120 | | 1121 | v 1122 | +-----------------+ +--------------------+ 1123 | | | | REQ套接字中,执行 | 1124 | | 是否在使用REQ | | send和recv,查看返 | 1125 | | 和REP套接字? +------->| 回结果。 | 1126 | | | Yes | REP套接字中,执行 | 1127 | | {o} | | recv和send。 | 1128 | +--------+--------+ +--------------------+ 1129 | | No 1130 | | 1131 | v 1132 | +-----------------+ +---------------------+ +-----------------+ 1133 | | | | 第一个连接成功的 | | 在发送任务之前 | 1134 | | 是否在使用PUSH | | PULL可能将所有任务 | | 需要额外的工作 | 1135 | | 套接字? +------->| 都接收下了,其他 +------->| 来同步套接字。 | 1136 | | {o} | | PULL接收不到。 | | | 1137 | +--------+--------+ +---------------------+ +-----------------+ 1138 | | No 1139 | | 1140 | v 1141 | +-----------------+ +-----------------+ 1142 | | | | | 1143 | | 是否检查了所有 | | 检查所有ZMQ方法 | 1144 | | 返回码? +------->| 的返回码,C语言 | 1145 | | | No | 中用assert断言。| 1146 | | {o} | | | 1147 | +--------+--------+ +-----------------+ 1148 | | Yes 1149 | | 1150 | v 1151 | +-----------------+ +-----------------+ +------------------+ 1152 | | | | | | | 1153 | | 是否在应用程序 | | 有没有在线程间 | | 为线程创建各自的 | 1154 | | 中使用了多线程? +------->| 传递套接字对象? +------->| 套接字对象。 | 1155 | | | Yes | | | | 1156 | | {o} | | {o} | | | 1157 | +--------+--------+ +--------+--------+ +------------------+ 1158 | | No | No 1159 | +--------------------------+ 1160 | | 1161 | v 1162 | +-----------------+ +-----------------+ +------------------+ 1163 | | | | | | | 1164 | | 是否使用了inproc| | 是否多次调用了 | | 每个进程只调用一 | 1165 | | 传输协议? +------->| zmq_init()? +------->| 次zmq_init()。 | 1166 | | | Yes | | Yes | | 1167 | | {o} | | {o} | | | 1168 | +--------+--------+ +--------+--------+ +------------------+ 1169 | | No | No 1170 | | | 1171 | | v 1172 | | +-----------------+ 1173 | | | | 1174 | | | 检查是否事先绑 | 1175 | | | 定到端点再连接 | 1176 | | | 到端点。 | 1177 | | | | 1178 | | +-----------------+ 1179 | | 1180 | v 1181 | +-----------------+ +-----------------+ +-----------------+ 1182 | | | | 检查应当地址是否| | 若使用了持久化套| 1183 | | 是否使用了 | | 合法的。ZMQ回直 | | 接字标识,检查它| 1184 | | ROUTER套接字? +------->| 接丢弃无法路由的+------->| 是否在连接之前设| 1185 | | | Yes | 消息。 | | 置的,而非连接后| 1186 | | {o} | | | | | 1187 | +--------+--------+ +-----------------+ +--------+--------+ 1188 | | No 1189 | | 1190 | v 1191 | +-----------------+ +--------------------+ 1192 | | | | | 1193 | | 是否只有零星的 | | 可能后台有其他客户 | 1194 | | 消息丢失? +------->| 端在运行,找出来并 | 1195 | | | Yes | 终止它。 | 1196 | | {o} | | | 1197 | +--------+--------+ +--------------------+ 1198 | | No 1199 | | 1200 | v 1201 | +-----------------+ 1202 | | | 1203 | | 写一个最简单的测| 1204 | | 试用例,到ZMQ的 | 1205 | | IRC频道上求助。 | 1206 | | | 1207 | +-----------------+ 1208 | 1209 | 1210 | Figure # - Missing Message Problem Solver 1211 | ``` 1212 | 1213 | 如果ZMQ在你的应用程序中扮演非常重要的角色,那你可能就需要好好计划一下了。首先,创建一个原型,用以测试设计方案的可行性。采取一些压力测试的手段,确保它足够的健壮。其次,主攻测试代码,也就是编写测试框架,保证有足够的电力供应和时间,来进行高强度的测试。理想状态下,应该由一个团队编写程序,另一个团队负责击垮它。最后,让你的公司及时[联系iMatix](http://www.imatix.com/contact),获得技术上的支持。 1214 | 1215 | 简而言之,如果你没有足够理由说明设计出来的架构能够在现实环境中运行,那么很有可能它就会在最紧要的关头崩溃。 1216 | 1217 | ### 警告:你的想法可能会被颠覆! 1218 | 1219 | 传统网络编程的一个规则是套接字只能和一个节点建立连接。虽然也有广播的协议,但毕竟是第三方的。当我们认定“一个套接字 = 一个连接”的时候,我们会用一些特定的方式来扩展应用程序架构:我们为每一块逻辑创建线程,该线程独立地维护一个套接字。 1220 | 1221 | 但在ZMQ的世界里,套接字是智能的、多线程的,能够自动地维护一组完整的连接。你无法看到它们,甚至不能直接操纵这些连接。当你进行消息的收发、轮询等操作时,只能和ZMQ套接字打交道,而不是连接本身。所以说,ZMQ世界里的连接是私有的,不对外部开放,这也是ZMQ易于扩展的原因之一。 1222 | 1223 | 由于你的代码只会和某个套接字进行通信,这样就可以处理任意多个连接,使用任意一种网络协议。而ZMQ的消息模式又可以进行更为廉价和便捷的扩展。 1224 | 1225 | 这样一来,传统的思维就无法在ZMQ的世界里应用了。在你阅读示例程序代码的时候,也许你脑子里会想方设法地将这些代码和传统的网络编程相关联:当你读到“套接字”的时候,会认为它就表示与另一个节点的连接——这种想法是错误的;当你读到“线程”时,会认为它是与另一个节点的连接——这也是错误的。 1226 | 1227 | 如果你是第一次阅读本指南,使用ZMQ进行了一两天的开发(或者更长),可能会觉得疑惑,ZMQ怎么会让事情便得如此简单。你再次尝试用以往的思维去理解ZMQ,但又无功而返。最后,你会被ZMQ的理念所折服,拨云见雾,开始享受ZMQ带来的乐趣。 1228 | 1229 | [iMatix]: http://www.imatix.com/ 1230 | [AMQP]: http://www.amqp.org/ 1231 | 1232 | -------------------------------------------------------------------------------- /chapter2.md: -------------------------------------------------------------------------------- 1 | ## 第二章 ZeroMQ进阶 2 | 3 | 第一章我们简单试用了ZMQ的若干通信模式:请求-应答模式、发布-订阅模式、管道模式。这一章我们将学习更多在实际开发中会使用到的东西: 4 | 5 | 本章涉及的内容有: 6 | 7 | * 创建和使用ZMQ套接字 8 | * 使用套接字发送和接收消息 9 | * 使用ZMQ提供的异步I/O套接字构建你的应用程序 10 | * 在单一线程中使用多个套接字 11 | * 恰当地处理致命和非致命错误 12 | * 处理诸如Ctrl-C的中断信号 13 | * 正确地关闭ZMQ应用程序 14 | * 检查ZMQ应用程序的内存泄露 15 | * 发送和接收多帧消息 16 | * 在网络中转发消息 17 | * 建立简单的消息队列代理 18 | * 使用ZMQ编写多线程应用程序 19 | * 使用ZMQ在线程间传递信号 20 | * 使用ZMQ协调网络中的节点 21 | * 使用标识创建持久化套接字 22 | * 在发布-订阅模式中创建和使用消息信封 23 | * 如何让持久化的订阅者能够从崩溃中恢复 24 | * 使用阈值(HWM)防止内存溢出 25 | 26 | ### 零的哲学 27 | 28 | ØMQ一词中的Ø让我们纠结了很久。一方面,这个特殊字符会降低ZMQ在谷歌和推特中的收录量;另一方面,这会惹恼某些丹麦语种的民族,他们会嚷道Ø并不是一个奇怪的0。 29 | 30 | 一开始ZMQ代表零中间件、零延迟,同时,它又有了新的含义:零管理、零成本、零浪费。总的来说,零表示最小、最简,这是贯穿于该项目的哲理。我们致力于减少复杂程度,提高易用性。 31 | 32 | ### 套接字API 33 | 34 | 说实话,ZMQ有些偷梁换柱的嫌疑。不过我们并不会为此道歉,因为这种概念上的切换绝对不会有坏处。ZMQ提供了一套类似于BSD套接字的API,但将很多消息处理机制的细节隐藏了起来,你会逐渐适应这种变化,并乐于用它进行编程。 35 | 36 | 套接字事实上是用于网络编程的标准接口,ZMQ之所那么吸引人眼球,原因之一就是它是建立在标准套接字API之上。因此,ZMQ的套接字操作非常容易理解,其生命周期主要包含四个部分: 37 | 38 | * 创建和销毁套接字:zmq_socket(), zmq_close() 39 | * 配置和读取套接字选项:zmq_setsockopt(), zmq_getsockopt() 40 | * 为套接字建立连接:zmq_bind(), zmq_connect() 41 | * 发送和接收消息:zmq_send(), zmq_recv() 42 | 43 | 如以下C代码: 44 | 45 | ```c 46 | void *mousetrap; 47 | 48 | // Create socket for catching mice 49 | mousetrap = zmq_socket (context, ZMQ_PULL); 50 | 51 | // Configure the socket 52 | int64_t jawsize = 10000; 53 | zmq_setsockopt (mousetrap, ZMQ_HWM, &jawsize, sizeof jawsize); 54 | 55 | // Plug socket into mouse hole 56 | zmq_connect (mousetrap, "tcp://192.168.55.221:5001"); 57 | 58 | // Wait for juicy mouse to arrive 59 | zmq_msg_t mouse; 60 | zmq_msg_init (&mouse); 61 | zmq_recv (mousetrap, &mouse, 0); 62 | // Destroy the mouse 63 | zmq_msg_close (&mouse); 64 | 65 | // Destroy the socket 66 | zmq_close (mousetrap); 67 | ``` 68 | 69 | 请注意,套接字永远是空指针类型的,而消息则是一个数据结构(我们下文会讲述)。所以,在C语言中你通过变量传递套接字,而用引用传递消息。记住一点,在ZMQ中所有的套接字都是由ZMQ管理的,只有消息是由程序员管理的。 70 | 71 | 创建、销毁、以及配置套接字的工作和处理一个对象差不多,但请记住ZMQ是异步的,伸缩性很强,因此在将其应用到网络结构中时,可能会需要多一些时间来理解。 72 | 73 | ### 使用套接字构建拓扑结构 74 | 75 | 在连接两个节点时,其中一个需要使用zmq_bind(),另一个则使用zmq_connect()。通常来讲,使用zmq_bind()连接的节点称之为服务端,它有着一个较为固定的网络地址;使用zmq_connect()连接的节点称为客户端,其地址不固定。我们会有这样的说法:绑定套接字至端点;连接套接字至端点。端点指的是某个广为周知网络地址。 76 | 77 | ZMQ连接和传统的TCP连接是有区别的,主要有: 78 | 79 | * 使用多种协议,inproc(进程内)、ipc(进程间)、tcp、pgm(广播)、epgm; 80 | * 当客户端使用zmq_connect()时连接就已经建立了,并不要求该端点已有某个服务使用zmq_bind()进行了绑定; 81 | * 连接是异步的,并由一组消息队列做缓冲; 82 | * 连接会表现出某种消息模式,这是由创建连接的套接字类型决定的; 83 | * 一个套接字可以有多个输入和输出连接; 84 | * ZMQ没有提供类似zmq_accept()的函数,因为当套接字绑定至端点时它就自动开始接受连接了; 85 | * 应用程序无法直接和这些连接打交道,因为它们是被封装在ZMQ底层的。 86 | 87 | 在很多架构中都使用了类似于C/S的架构。服务端组件是比较稳定的,而客户端组件则较为动态,来去自如。所以说,服务端地址对客户端而言往往是可见的,反之则不然。这样一来,架构中应该将哪些组件作为服务端(使用zmq_bind()),哪些作为客户端(使用zmq_connect()),就很明显了。同时,这需要和你使用的套接字类型相联系起来,我们下文会详细讲述。 88 | 89 | 让我们试想一下,如果先打开了客户端,后打开服务端,会发生什么?传统网络连接中,我们打开客户端时一定会收到系统的报错信息,但ZMQ让我们能够自由地启动架构中的组件。当客户端使用zmq_connect()连接至某个端点时,它就已经能够使用该套接字发送消息了。如果这时,服务端启动起来了,并使用zmq_bind()绑定至该端点,ZMQ将自动开始转发消息。 90 | 91 | 服务端节点可以仅使用一个套接字就能绑定至多个端点。也就是说,它能够使用不同的协议来建立连接: 92 | 93 | ```c 94 | zmq_bind (socket, "tcp://*:5555"); 95 | zmq_bind (socket, "tcp://*:9999"); 96 | zmq_bind (socket, "ipc://myserver.ipc"); 97 | ``` 98 | 99 | 当然,你不能多次绑定至同一端点,这样是会报错的。 100 | 101 | 每当有客户端节点使用zmq_connect()连接至上述某个端点时,服务端就会自动创建连接。ZMQ没有对连接数量进行限制。此外,客户端节点也可以使用一个套接字同时建立多个连接。 102 | 103 | 大多数情况下,哪个节点充当服务端,哪个作为客户端,是网络架构层面的内容,而非消息流问题。不过也有一些特殊情况(如失去连接后的消息重发),同一种套接字使用绑定和连接是会有一些不同的行为的。 104 | 105 | 所以说,当我们在设计架构时,应该遵循“服务端是稳定的,客户端是灵活的“原则,这样就不太会出错。 106 | 107 | 套接字是有类型的,套接字类型定义了套接字的行为,它在发送和接收消息时的规则等。你可以将不同种类的套接字进行连接,如PUB-SUB组合,这种组合称之为发布-订阅模式,其他组合也会有相应的模式名称,我们会在下文详述。 108 | 109 | 正是因为套接字可以使用不同的方式进行连接,才构成了ZMQ最基本的消息队列系统。我们还可以在此基础之上建立更为复杂的装置、路由机制等,下文会详述。总的来说,ZMQ为你提供了一套组件,供你在网络架构中拼装和使用。 110 | 111 | ### 使用套接字传递数据 112 | 113 | 发送和接收消息使用的是zmq_send()和zmq_recv()这两个函数。虽然函数名称看起来很直白,但由于ZMQ的I/O模式和传统的TCP协议有很大不同,因此还是需要花点时间去理解的。 114 | 115 | ![1](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_1.png) 116 | 117 | 让我们看一看TCP套接字和ZMQ套接字之间在传输数据方面的区别: 118 | 119 | * ZMQ套接字传输的是消息,而不是字节(TCP)或帧(UDP)。消息指的是一段指定长度的二进制数据块,我们下文会讲到消息,这种设计是为了性能优化而考虑的,所以可能会比较难以理解。 120 | * ZMQ套接字在后台进行I/O操作,也就是说无论是接收还是发送消息,它都会先传送到一个本地的缓冲队列,这个内存队列的大小是可以配置的。 121 | * ZMQ套接字可以和多个套接字进行连接(如果套接字类型允许的话)。TCP协议只能进行点对点的连接,而ZMQ则可以进行一对多(类似于无线广播)、多对多(类似于邮局)、多对一(类似于信箱),当然也包括一对一的情况。 122 | * ZMQ套接字可以发送消息给多个端点(扇出模型),或从多个端点中接收消息(扇入模型) 123 | 124 | ![2](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_2.png) 125 | 126 | 所以,向套接字写入一个消息时可能会将消息发送给很多节点,相应的,套接字又会从所有已建立的连接中接收消息。zmq_recv()方法使用了公平队列的算法来决定接收哪个连接的消息。 127 | 128 | 调用zmq_send()方法时其实并没有真正将消息发送给套接字连接。消息会在一个内存队列中保存下来,并由后台的I/O线程异步地进行发送。如果不出意外情况,这一行为是非阻塞的。所以说,即便zmq_send()有返回值,并不能代表消息已经发送。当你在用zmq_msg_init_data()初始化消息后,你不能重用或是释放这条消息,否则ZMQ的I/O线程会认为它在传输垃圾数据。这对初学者来讲是一个常犯的错误,下文我们会讲述如何正确地处理消息。 129 | 130 | ### 单播传输 131 | 132 | ZMQ提供了一组单播传输协议(inporc, ipc, tcp),和两个广播协议(epgm, pgm)。广播协议是比较高级的协议,我们会在以后讲述。如果你不能回答我扇出比例会影响一对多的单播传输时,就先不要去学习广播协议了吧。 133 | 134 | 一般而言我们会使用**tcp**作为传输协议,这种TCP连接是可以脱机运作的,它灵活、便携、且足够快速。为什么称之为脱机,是因为ZMQ中的TCP连接不需要该端点已经有某个服务进行了绑定,客户端和服务端可以随时进行连接和绑定,这对应用程序而言都是透明的。 135 | 136 | 进程间协议,即**ipc**,和tcp的行为差不多,但已从网络传输中抽象出来,不需要指定IP地址或者域名。这种协议很多时候会很方便,本指南中的很多示例都会使用这种协议。ZMQ中的ipc协议同样可以是脱机的,但有一个缺点——无法在Windows操作系统上运作,这一点也许会在未来的ZMQ版本中修复。我们一般会在端点名称的末尾附上.ipc的扩展名,在UNIX系统上,使用ipc协议还需要注意权限问题。你还需要保证所有的程序都能够找到这个ipc端点。 137 | 138 | 进程内协议,即**inproc**,可以在同一个进程的不同线程之间进行消息传输,它比ipc或tcp要快得多。这种协议有一个要求,必须先绑定到端点,才能建立连接,也许未来也会修复。通常的做法是先启动服务端线程,绑定至端点,后启动客户端线程,连接至端点。 139 | 140 | ### ZMQ不只是数据传输 141 | 142 | 经常有新人会问,如何使用ZMQ建立一项服务?我能使用ZMQ建立一个HTTP服务器吗? 143 | 144 | 他们期望得到的回答是,我们用普通的套接字来传输HTTP请求和应答,那用ZMQ套接字也能够完成这个任务,且能运行得更快、更好。 145 | 146 | 只可惜答案并不是这样的。ZMQ不只是一个数据传输的工具,而是在现有通信协议之上建立起来的新架构。它的数据帧和现有的协议并不兼容,如下面是一个HTTP请求和ZMQ请求的对比,同样使用的是TCP/IPC协议: 147 | 148 | ![3](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_3.png) 149 | 150 | HTTP请求使用CR-LF(换行符)作为信息帧的间隔,而ZMQ则使用指定长度来定义帧: 151 | 152 | ![4](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_4.png) 153 | 154 | 所以说,你的确是可以用ZMQ来写一个类似于HTTP协议的东西,但是这并不是HTTP。 155 | 156 | 不过,如果有人问我如何更好地使用ZMQ建立一个新的服务,我会给出一个不错的答案,那就是:你可以自行设计一种通信协议,用ZMQ进行连接,使用不同的语言提供服务和扩展,可以在本地,亦可通过远程传输。赛德•肖的[Mongrel2](http://www.mongrel2.org)网络服务的架构就是一个很好的示例。 157 | 158 | ### I/O线程 159 | 160 | 我们提过ZMQ是通过后台的I/O线程进行消息传输的。一个I/O线程已经足以处理多个套接字的数据传输要求,当然,那些极端的应用程序除外。这也就是我们在创建上下文时传入的1所代表的意思: 161 | 162 | ```c 163 | void *context = zmq_init (1); 164 | ``` 165 | 166 | ZMQ应用程序和传统应用程序的区别之一就是你不需要为每个套接字都创建一个连接。单个ZMQ套接字可以处理所有的发送和接收任务。如,当你需要向一千个订阅者发布消息时,使用一个套接字就可以了;当你需要向二十个服务进程分发任务时,使用一个套接字就可以了;当你需要从一千个网页应用程序中获取数据时,也是使用一个套接字就可以了。 167 | 168 | 这一特性可能会颠覆网络应用程序的编写步骤,传统应用程序每个进程或线程会有一个远程连接,它又只能处理一个套接字。ZMQ让你打破这种结构,使用一个线程来完成所有工作,更易于扩展。 169 | 170 | ### 核心消息模式 171 | 172 | ZMQ的套接字API中提供了多种消息模式。如果你熟悉企业级消息应用,那这些模式会看起来很熟悉。不过对于新手来说,ZMQ的套接字还是会让人大吃一惊的。 173 | 174 | 让我们回顾一下ZMQ会为你做些什么:它会将消息快速高效地发送给其他节点,这里的节点可以是线程、进程、或是其他计算机;ZMQ为应用程序提供了一套简单的套接字API,不用考虑实际使用的协议类型(进程内、进程间、TPC、或广播);当节点调动时,ZMQ会自动进行连接或重连;无论是发送消息还是接收消息,ZMQ都会先将消息放入队列中,并保证进程不会因为内存溢出而崩溃,适时地将消息写入磁盘;ZMQ会处理套接字异常;所有的I/O操作都在后台进行;ZMQ不会产生死锁。 175 | 176 | 但是,以上种种的前提是用户能够正确地使用消息模式,这种模式往往也体现出了ZMQ的智慧。消息模式将我们从实践中获取的经验进行抽象和重组,用于解决之后遇到的所有问题。ZMQ的消息模式目前是编译在类库中的,不过未来的ZMQ版本可能会允许用户自行制定消息模式。 177 | 178 | ZMQ的消息模式是指不同类型套接字的组合。换句话说,要理解ZMQ的消息模式,你需要理解ZMQ的套接字类型,它们是如何一起工作的。这一部分是需要死记硬背的。 179 | 180 | ZMQ的核心消息模式有: 181 | 182 | * **请求-应答模式** 将一组服务端和一组客户端相连,用于远程过程调用或任务分发。 183 | 184 | * **发布-订阅模式** 将一组发布者和一组订阅者相连,用于数据分发。 185 | 186 | * **管道模式** 使用扇入或扇出的形式组装多个节点,可以产生多个步骤或循环,用于构建并行处理架构。 187 | 188 | 我们在第一章中已经讲述了这些模式,不过还有一种模式是为那些仍然认为ZMQ是类似TCP那样点对点连接的人们准备的: 189 | 190 | * **排他对接模式** 将两个套接字一对一地连接起来,这种模式应用场景很少,我们会在本章最末尾看到一个示例。 191 | 192 | zmq_socket()函数的说明页中有对所有消息模式的说明,比较清楚,因此值得研读几次。我们会介绍每种消息模式的内容和应用场景。 193 | 194 | 以下是合法的套接字连接-绑定对(一端绑定、一端连接即可): 195 | 196 | * PUB - SUB 197 | * REQ - REP 198 | * REQ - ROUTER 199 | * DEALER - REP 200 | * DEALER - ROUTER 201 | * DEALER - DEALER 202 | * ROUTER - ROUTER 203 | * PUSH - PULL 204 | * PAIR - PAIR 205 | 206 | 其他的组合模式会产生不可预知的结果,在将来的ZMQ版本中可能会直接返回错误。你也可以通过代码去了解这些套接字类型的行为。 207 | 208 | ### 上层消息模式 209 | 210 | 上文中的四种核心消息模式是内建在ZMQ中的,他们是API的一部分,在ZMQ的C++核心类库中实现,能够保证正确地运行。如果有朝一日Linux内核将ZMQ采纳了进来,那这些核心模式也肯定会包含其中。 211 | 212 | 在这些消息模式之上,我们会建立更为上层的消息模式。这种模式可以用任何语言编写,他们不属于核心类型的一部分,不随ZMQ发行,只在你自己的应用程序中出现,或者在ZMQ社区中维护。 213 | 214 | 本指南的目的之一就是为你提供一些上层的消息模式,有简单的(如何正确处理消息),也有复杂的(可靠的发布-订阅模式)。 215 | 216 | ### 消息的使用方法 217 | 218 | ZMQ的传输单位是消息,即一个二进制块。你可以使用任意的序列化工具,如谷歌的Protocal Buffers、XDR、JSON等,将内容转化成ZMQ消息。不过这种转化工具最好是便捷和快速的,这个请自己衡量。 219 | 220 | 在内存中,ZMQ消息由zmq_msg_t结构表示(每种语言有特定的表示)。在C语言中使用ZMQ消息时需要注意以下几点: 221 | 222 | * 你需要创建和传递zmq_msg_t对象,而不是一组数据块; 223 | * 读取消息时,先用zmq_msg_init()初始化一个空消息,再将其传递给zmq_recv()函数; 224 | * 写入消息时,先用zmq_msg_init_size()来创建消息(同时也已初始化了一块内存区域),然后用memcpy()函数将信息拷贝到该对象中,最后传给zmq_send()函数; 225 | * 释放消息(并不是销毁)时,使用zmq_msg_close()函数,它会将对消息对象的引用删除,最终由ZMQ将消息销毁; 226 | * 获取消息内容时需使用zmq_msg_data()函数;若想知道消息的长度,可以使用zmq_msg_size()函数; 227 | * 至于zmq_msg_move()、zmq_msg_copy()、zmq_msg_init_data()函数,在充分理解手册中的说明之前,建议不好贸然使用。 228 | 229 | 以下是一段处理消息的典型代码,如果之前的代码你有看的话,那应该会感到熟悉。这段代码其实是从zhelpers.h文件中抽出的: 230 | 231 | ```c 232 | // 从套接字中获取ZMQ字符串,并转换为C语言字符串 233 | static char * 234 | s_recv (void *socket) { 235 | zmq_msg_t message; 236 | zmq_msg_init (&message); 237 | zmq_recv (socket, &message, 0); 238 | int size = zmq_msg_size (&message); 239 | char *string = malloc (size + 1); 240 | memcpy (string, zmq_msg_data (&message), size); 241 | zmq_msg_close (&message); 242 | string [size] = 0; 243 | return (string); 244 | } 245 | 246 | // 将C语言字符串转换为ZMQ字符串,并发送给套接字 247 | static int 248 | s_send (void *socket, char *string) { 249 | int rc; 250 | zmq_msg_t message; 251 | zmq_msg_init_size (&message, strlen (string)); 252 | memcpy (zmq_msg_data (&message), string, strlen (string)); 253 | rc = zmq_send (socket, &message, 0); 254 | assert (!rc); 255 | zmq_msg_close (&message); 256 | return (rc); 257 | } 258 | ``` 259 | 260 | 你可以对以上代码进行扩展,让其支持发送和接受任一长度的数据。 261 | 262 | **需要注意的是,当你将一个消息对象传递给zmq_send()函数后,该对象的长度就会被清零,因此你无法发送同一个消息对象两次,也无法获得已发送消息的内容。** 263 | 264 | 如果你想发送同一个消息对象两次,就需要在发送第一次前新建一个对象,使用zmq_msg_copy()函数进行拷贝。这个函数不会拷贝消息内容,只是拷贝引用。然后你就可以再次发送这个消息了(或者任意多次,只要进行了足够的拷贝)。当消息最后一个引用被释放时,消息对象就会被销毁。 265 | 266 | ZMQ支持多帧消息,即在一条消息中保存多个消息帧。这在实际应用中被广泛使用,我们会在第三章进行讲解。 267 | 268 | 关于消息,还有一些需要注意的地方: 269 | 270 | * ZMQ的消息是作为一个整体来收发的,你不会只收到消息的一部分; 271 | * ZMQ不会立即发送消息,而是有一定的延迟; 272 | * 你可以发送0字节长度的消息,作为一种信号; 273 | * 消息必须能够在内存中保存,如果你想发送文件或超长的消息,就需要将他们切割成小块,在独立的消息中进行发送; 274 | * 必须使用zmq_msg_close()函数来关闭消息,但在一些会在变量超出作用域时自动释放消息对象的语言中除外。 275 | 276 | 再重复一句,不要贸然使用zmq_msg_init_data()函数。它是用于零拷贝,而且可能会造成麻烦。关于ZMQ还有太多东西需要你去学习,因此现在暂时不用去考虑如何削减几微秒的开销。 277 | 278 | ### 处理多个套接字 279 | 280 | 在之前的示例中,主程序的循环体内会做以下几件事: 281 | 282 | 1. 等待套接字的消息; 283 | 1. 处理消息; 284 | 1. 返回第一步。 285 | 286 | 如果我们想要读取多个套接字中的消息呢?最简单的方法是将套接字连接到多个端点上,让ZMQ使用公平队列的机制来接受消息。如果不同端点上的套接字类型是一致的,那可以使用这种方法。但是,如果一个套接字的类型是PULL,另一个是PUB怎么办?如果现在开始混用套接字类型,那将来就没有可靠性可言了。 287 | 288 | 正确的方法应该是使用zmq_poll()函数。更好的方法是将zmq_poll()包装成一个框架,编写一个事件驱动的反应器,但这个就比较复杂了,我们这里暂不讨论。 289 | 290 | 我们先不使用zmq_poll(),而用NOBLOCK(非阻塞)的方式来实现从多个套接字读取消息的功能。下面将气象信息服务和并行处理这两个示例结合起来: 291 | 292 | **msreader: Multiple socket reader in C** 293 | 294 | ```c 295 | // 296 | // 从多个套接字中获取消息 297 | // 本示例简单地再循环中使用recv函数 298 | // 299 | #include "zhelpers.h" 300 | 301 | int main (void) 302 | { 303 | // 准备上下文和套接字 304 | void *context = zmq_init (1); 305 | 306 | // 连接至任务分发器 307 | void *receiver = zmq_socket (context, ZMQ_PULL); 308 | zmq_connect (receiver, "tcp://localhost:5557"); 309 | 310 | // 连接至天气服务 311 | void *subscriber = zmq_socket (context, ZMQ_SUB); 312 | zmq_connect (subscriber, "tcp://localhost:5556"); 313 | zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "10001 ", 6); 314 | 315 | // 处理从两个套接字中接收到的消息 316 | // 这里我们会优先处理从任务分发器接收到的消息 317 | while (1) { 318 | // 处理等待中的任务 319 | int rc; 320 | for (rc = 0; !rc; ) { 321 | zmq_msg_t task; 322 | zmq_msg_init (&task); 323 | if ((rc = zmq_recv (receiver, &task, ZMQ_NOBLOCK)) == 0) { 324 | // 处理任务 325 | } 326 | zmq_msg_close (&task); 327 | } 328 | // 处理等待中的气象更新 329 | for (rc = 0; !rc; ) { 330 | zmq_msg_t update; 331 | zmq_msg_init (&update); 332 | if ((rc = zmq_recv (subscriber, &update, ZMQ_NOBLOCK)) == 0) { 333 | // 处理气象更新 334 | } 335 | zmq_msg_close (&update); 336 | } 337 | // 没有消息,等待1毫秒 338 | s_sleep (1); 339 | } 340 | // 程序不会运行到这里,但还是做正确的退出清理工作 341 | zmq_close (receiver); 342 | zmq_close (subscriber); 343 | zmq_term (context); 344 | return 0; 345 | } 346 | ``` 347 | 348 | 这种方式的缺点之一是,在收到第一条消息之前会有1毫秒的延迟,这在高压力的程序中还是会构成问题的。此外,你还需要翻阅诸如nanosleep()的函数,不会造成循环次数的激增。 349 | 350 | 示例中将任务分发器的优先级提升了,你可以做一个改进,轮流处理消息,正如ZMQ内部做的公平队列机制一样。 351 | 352 | 下面,让我们看看如何用zmq_poll()来实现同样的功能: 353 | 354 | **mspoller: Multiple socket poller in C** 355 | 356 | ```c 357 | // 358 | // 从多个套接字中接收消息 359 | // 本例使用zmq_poll()函数 360 | // 361 | #include "zhelpers.h" 362 | 363 | int main (void) 364 | { 365 | void *context = zmq_init (1); 366 | 367 | // 连接任务分发器 368 | void *receiver = zmq_socket (context, ZMQ_PULL); 369 | zmq_connect (receiver, "tcp://localhost:5557"); 370 | 371 | // 连接气象更新服务 372 | void *subscriber = zmq_socket (context, ZMQ_SUB); 373 | zmq_connect (subscriber, "tcp://localhost:5556"); 374 | zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "10001 ", 6); 375 | 376 | // 初始化轮询对象 377 | zmq_pollitem_t items [] = { 378 | { receiver, 0, ZMQ_POLLIN, 0 }, 379 | { subscriber, 0, ZMQ_POLLIN, 0 } 380 | }; 381 | // 处理来自两个套接字的消息 382 | while (1) { 383 | zmq_msg_t message; 384 | zmq_poll (items, 2, -1); 385 | if (items [0].revents & ZMQ_POLLIN) { 386 | zmq_msg_init (&message); 387 | zmq_recv (receiver, &message, 0); 388 | // 处理任务 389 | zmq_msg_close (&message); 390 | } 391 | if (items [1].revents & ZMQ_POLLIN) { 392 | zmq_msg_init (&message); 393 | zmq_recv (subscriber, &message, 0); 394 | // 处理气象更新 395 | zmq_msg_close (&message); 396 | } 397 | } 398 | // 程序不会运行到这儿 399 | zmq_close (receiver); 400 | zmq_close (subscriber); 401 | zmq_term (context); 402 | return 0; 403 | } 404 | ``` 405 | 406 | ### 处理错误和ETERM信号 407 | 408 | ZMQ的错误处理机制提倡的是快速崩溃。我们认为,一个进程对于自身内部的错误来说要越脆弱越好,而对外部的攻击和错误要足够健壮。举个例子,活细胞会因检测到自身问题而瓦解,但对外界的攻击却能极力抵抗。在ZMQ编程中,断言用得是非常多的,如同细胞膜一样。如果我们无法确定一个错误是来自于内部还是外部,那这就是一个设计缺陷了,需要修复。 409 | 410 | 在C语言中,断言失败会让程序立即中止。其他语言中可以使用异常来做到。 411 | 412 | 当ZMQ检测到来自外部的问题时,它会返回一个错误给调用程序。如果ZMQ不能从错误中恢复,那它是不会安静地将消息丢弃的。某些情况下,ZMQ也会去断言外部错误,这些可以被归结为BUG。 413 | 414 | 到目前为止,我们很少看到C语言的示例中有对错误进行处理。**现实中的代码应该对每一次的ZMQ函数调用作错误处理**。如果你不是使用C语言进行编程,可能那种语言的ZMQ类库已经做了错误处理。但在C语言中,你需要自己动手。以下是一些常规的错误处理手段,从POSIX规范开始: 415 | 416 | * 创建对象的方法如果失败了会返回NULL; 417 | * 其他方法执行成功时会返回0,失败时会返回其他值(一般是-1); 418 | * 错误代码可以从变量errno中获得,或者调用zmq_errno()函数; 419 | * 错误消息可以调用zmq_strerror()函数获得。 420 | 421 | 有两种情况不应该被认为是错误: 422 | 423 | * 当线程使用NOBLOCK方式调用zmq_recv()时,若没有接收到消息,该方法会返回-1,并设置errno为EAGAIN; 424 | * 当线程调用zmq_term()时,若其他线程正在进行阻塞式的处理,该函数会中止所有的处理,关闭套接字,并使得那些阻塞方法的返回值为-1,errno设置为ETERM。 425 | 426 | 遵循以上规则,你就可以在ZMQ程序中使用断言了: 427 | 428 | ```c 429 | void *context = zmq_init (1); 430 | assert (context); 431 | void *socket = zmq_socket (context, ZMQ_REP); 432 | assert (socket); 433 | int rc; 434 | rc = zmq_bind (socket, "tcp://*:5555"); 435 | assert (rc == 0); 436 | ``` 437 | 438 | 第一版的程序中我将函数调用直接放在了assert()函数里面,这样做会有问题,因为一些优化程序会直接将程序中的assert()函数去除。 439 | 440 | 让我们看看如何正确地关闭一个进程,我们用管道模式举例。当我们在后台开启了一组worker时,我们需要在任务执行完毕后关闭它们。我们可以向这些worker发送自杀的消息,这项工作由结果收集器来完成会比较恰当。 441 | 442 | 如何将结果收集器和worker相连呢?PUSH-PULL套接字是单向的。ZMQ的原则是:如果需要解决一个新的问题,就该使用新的套接字。这里我们使用发布-订阅模式来发送自杀的消息: 443 | 444 | * 结果收集器创建PUB套接字,并连接至一个新的端点; 445 | * worker将SUB套接字连接至这个端点; 446 | * 当结果收集器检测到任务执行完毕时,会通过PUB套接字发送自杀信号; 447 | * worker收到自杀信号后便会中止。 448 | 449 | 这一过程不会添加太多的代码: 450 | 451 | ```c 452 | void *control = zmq_socket (context, ZMQ_PUB); 453 | zmq_bind (control, "tcp://*:5559"); 454 | ... 455 | // Send kill signal to workers 456 | zmq_msg_init_data (&message, "KILL", 5); 457 | zmq_send (control, &message, 0); 458 | zmq_msg_close (&message); 459 | ``` 460 | 461 | ![5](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_5.png) 462 | 463 | 下面是worker进程的代码,它会打开三个套接字:用于接收任务的PULL、用于发送结果的PUSH、以及用于接收自杀信号的SUB,使用zmq_poll()进行轮询: 464 | 465 | **taskwork2: Parallel task worker with kill signaling in C** 466 | 467 | ```c 468 | // 469 | // 管道模式 - worker 设计2 470 | // 添加发布-订阅消息流,用以接收自杀消息 471 | // 472 | #include "zhelpers.h" 473 | 474 | int main (void) 475 | { 476 | void *context = zmq_init (1); 477 | 478 | // 用于接收消息的套接字 479 | void *receiver = zmq_socket (context, ZMQ_PULL); 480 | zmq_connect (receiver, "tcp://localhost:5557"); 481 | 482 | // 用户发送消息的套接字 483 | void *sender = zmq_socket (context, ZMQ_PUSH); 484 | zmq_connect (sender, "tcp://localhost:5558"); 485 | 486 | // 用户接收控制消息的套接字 487 | void *controller = zmq_socket (context, ZMQ_SUB); 488 | zmq_connect (controller, "tcp://localhost:5559"); 489 | zmq_setsockopt (controller, ZMQ_SUBSCRIBE, "", 0); 490 | 491 | // 处理接收到的任务或控制消息 492 | zmq_pollitem_t items [] = { 493 | { receiver, 0, ZMQ_POLLIN, 0 }, 494 | { controller, 0, ZMQ_POLLIN, 0 } 495 | }; 496 | // 处理消息 497 | while (1) { 498 | zmq_msg_t message; 499 | zmq_poll (items, 2, -1); 500 | if (items [0].revents & ZMQ_POLLIN) { 501 | zmq_msg_init (&message); 502 | zmq_recv (receiver, &message, 0); 503 | 504 | // 工作 505 | s_sleep (atoi ((char *) zmq_msg_data (&message))); 506 | 507 | // 发送结果 508 | zmq_msg_init (&message); 509 | zmq_send (sender, &message, 0); 510 | 511 | // 简单的任务进图指示 512 | printf ("."); 513 | fflush (stdout); 514 | 515 | zmq_msg_close (&message); 516 | } 517 | // 任何控制命令都表示自杀 518 | if (items [1].revents & ZMQ_POLLIN) 519 | break; // 退出循环 520 | } 521 | // 结束程序 522 | zmq_close (receiver); 523 | zmq_close (sender); 524 | zmq_close (controller); 525 | zmq_term (context); 526 | return 0; 527 | } 528 | ``` 529 | 530 | 下面是修改后的结果收集器代码,在收集完结果后向所有worker发送自杀消息: 531 | 532 | **tasksink2: Parallel task sink with kill signaling in C** 533 | 534 | ```c 535 | // 536 | // 管道模式 - 结构收集器 设计2 537 | // 添加发布-订阅消息流,用以向worker发送自杀信号 538 | // 539 | #include "zhelpers.h" 540 | 541 | int main (void) 542 | { 543 | void *context = zmq_init (1); 544 | 545 | // 用于接收消息的套接字 546 | void *receiver = zmq_socket (context, ZMQ_PULL); 547 | zmq_bind (receiver, "tcp://*:5558"); 548 | 549 | // 用以发送控制信息的套接字 550 | void *controller = zmq_socket (context, ZMQ_PUB); 551 | zmq_bind (controller, "tcp://*:5559"); 552 | 553 | // 等待任务开始 554 | char *string = s_recv (receiver); 555 | free (string); 556 | 557 | // 开始计时 558 | int64_t start_time = s_clock (); 559 | 560 | // 确认100个任务处理完毕 561 | int task_nbr; 562 | for (task_nbr = 0; task_nbr < 100; task_nbr++) { 563 | char *string = s_recv (receiver); 564 | free (string); 565 | if ((task_nbr / 10) * 10 == task_nbr) 566 | printf (":"); 567 | else 568 | printf ("."); 569 | fflush (stdout); 570 | } 571 | printf ("总执行时间: %d msec\n", 572 | (int) (s_clock () - start_time)); 573 | 574 | // 发送自杀消息给worker 575 | s_send (controller, "KILL"); 576 | 577 | // 结束 578 | sleep (1); // 等待发送完毕 579 | 580 | zmq_close (receiver); 581 | zmq_close (controller); 582 | zmq_term (context); 583 | return 0; 584 | } 585 | ``` 586 | 587 | ### 处理中断信号 588 | 589 | 现实环境中,当应用程序收到Ctrl-C或其他诸如ETERM的信号时需要能够正确地清理和退出。默认情况下,这一信号会杀掉进程,意味着尚未发送的消息就此丢失,文件不能被正确地关闭等。 590 | 591 | 在C语言中我们是这样处理消息的: 592 | 593 | **interrupt: Handling Ctrl-C cleanly in C** 594 | 595 | ```c 596 | // 597 | // Shows how to handle Ctrl-C 598 | // 599 | #include 600 | #include 601 | #include 602 | 603 | // --------------------------------------------------------------------- 604 | // 消息处理 605 | // 606 | // 程序开始运行时调用s_catch_signals()函数; 607 | // 在循环中判断s_interrupted是否为1,是则跳出循环; 608 | // 很适用于zmq_poll()。 609 | 610 | static int s_interrupted = 0; 611 | static void s_signal_handler (int signal_value) 612 | { 613 | s_interrupted = 1; 614 | } 615 | 616 | static void s_catch_signals (void) 617 | { 618 | struct sigaction action; 619 | action.sa_handler = s_signal_handler; 620 | action.sa_flags = 0; 621 | sigemptyset (&action.sa_mask); 622 | sigaction (SIGINT, &action, NULL); 623 | sigaction (SIGTERM, &action, NULL); 624 | } 625 | 626 | int main (void) 627 | { 628 | void *context = zmq_init (1); 629 | void *socket = zmq_socket (context, ZMQ_REP); 630 | zmq_bind (socket, "tcp://*:5555"); 631 | 632 | s_catch_signals (); 633 | while (1) { 634 | // 阻塞式的读取会在收到信号时停止 635 | zmq_msg_t message; 636 | zmq_msg_init (&message); 637 | zmq_recv (socket, &message, 0); 638 | 639 | if (s_interrupted) { 640 | printf ("W: 收到中断消息,程序中止...\n"); 641 | break; 642 | } 643 | } 644 | zmq_close (socket); 645 | zmq_term (context); 646 | return 0; 647 | } 648 | ``` 649 | 650 | 这段程序使用s_catch_signals()函数来捕捉像Ctrl-C(SIGINT)和SIGTERM这样的信号。收到任一信号后,该函数会将全局变量s_interrupted设置为1。你的程序并不会自动停止,需要显式地做一些清理和退出工作。 651 | 652 | * 在程序开始时调用s_catch_signals()函数,用来进行信号捕捉的设置; 653 | * 如果程序在zmq_recv()、zmq_poll()、zmq_send()等函数中阻塞,当有信号传来时,这些函数会返回EINTR; 654 | * 像s_recv()这样的函数会将这种中断包装为NULL返回; 655 | * 所以,你的应用程序可以检查是否有EINTR错误码、或是NULL的返回、或者s_interrupted变量是否为1。 656 | 657 | 如以下代码就十分典型: 658 | 659 | ```c 660 | s_catch_signals (); 661 | client = zmq_socket (...); 662 | while (!s_interrupted) { 663 | char *message = s_recv (client); 664 | if (!message) 665 | break; // 按下了Ctrl-C 666 | } 667 | zmq_close (client); 668 | ``` 669 | 670 | 如果你在设置s_catch_signals()之后没有进行相应的处理,那么你的程序将对Ctrl-C和ETERM免疫。 671 | 672 | ### 检测内存泄露 673 | 674 | 任何长时间运行的程序都应该妥善的管理内存,否则最终会发生内存溢出,导致程序崩溃。如果你所使用的编程语言会自动帮你完成内存管理,那就要恭喜你了。但若你使用类似C/C++之类的语言时,就需要自己动手进行内存管理了。下面会介绍一个名为valgrind的工具,可以用它来报告内存泄露的问题。 675 | 676 | * 在Ubuntu或Debian操作系统上安装valgrind:sudo apt-get install valgrind 677 | 678 | * 缺省情况下,ZMQ会让valgrind不停地报错,想要屏蔽警告的话可以在编译ZMQ时使用ZMQ_MAKE_VALGRIND_HAPPY宏选项: 679 | 680 | ``` 681 | $ cd zeromq2 682 | $ export CPPFLAGS=-DZMQ_MAKE_VALGRIND_HAPPY 683 | $ ./configure 684 | $ make clean; make 685 | $ sudo make install 686 | ``` 687 | 688 | * 应用程序应该正确地处理Ctrl-C,特别是对于长时间运行的程序(如队列装置),如果不这么做,valgrind会报告所有已分配的内存发生了错误。 689 | 690 | * 使用-DDEBUG选项编译程序,这样可以让valgrind告诉你具体是哪段代码发生了内存溢出。 691 | 692 | * 最后,使用如下方法运行valgrind: 693 | 694 | ``` 695 | valgrind --tool=memcheck --leak-check=full someprog 696 | ``` 697 | 698 | 解决完所有的问题后,你会看到以下信息: 699 | 700 | ``` 701 | ==30536== ERROR SUMMARY: 0 errors from 0 contexts... 702 | ``` 703 | 704 | ### 多帧消息 705 | 706 | ZMQ消息可以包含多个帧,这在实际应用中非常常见,特别是那些有关“信封”的应用,我们下文会谈到。我们这一节要讲的是如何正确地收发多帧消息。 707 | 708 | 多帧消息的每一帧都是一个zmq_msg结构,也就是说,当你在收发含有五个帧的消息时,你需要处理五个zmq_msg结构。你可以将这些帧放入一个数据结构中,或者直接一个个地处理它们。 709 | 710 | 下面的代码演示如何发送多帧消息: 711 | 712 | ```c 713 | zmq_send (socket, &message, ZMQ_SNDMORE); 714 | ... 715 | zmq_send (socket, &message, ZMQ_SNDMORE); 716 | ... 717 | zmq_send (socket, &message, 0); 718 | ``` 719 | 720 | 然后我们看看如何接收并处理这些消息,这段代码对单帧消息和多帧消息都适用: 721 | 722 | ```c 723 | while (1) { 724 | zmq_msg_t message; 725 | zmq_msg_init (&message); 726 | zmq_recv (socket, &message, 0); 727 | // 处理一帧消息 728 | zmq_msg_close (&message); 729 | int64_t more; 730 | size_t more_size = sizeof (more); 731 | zmq_getsockopt (socket, ZMQ_RCVMORE, &more, &more_size); 732 | if (!more) 733 | break; // 已到达最后一帧 734 | } 735 | ``` 736 | 737 | 关于多帧消息,你需要了解的还有: 738 | 739 | * 在发送多帧消息时,只有当最后一帧提交发送了,整个消息才会被发送; 740 | * 如果使用了zmq_poll()函数,当收到了消息的第一帧时,其它帧其实也已经收到了; 741 | * 多帧消息是整体传输的,不会只收到一部分; 742 | * 多帧消息的每一帧都是一个zmq_msg结构; 743 | * 无论你是否检查套接字的ZMQ_RCVMORE选项,你都会收到所有的消息; 744 | * 发送时,ZMQ会将开始的消息帧缓存在内存中,直到收到最后一帧才会发送; 745 | * 我们无法在发送了一部分消息后取消发送,只能关闭该套接字。 746 | 747 | ### 中间件和装置 748 | 749 | 当网络组件的数量较少时,所有节点都知道其它节点的存在。但随着节点数量的增加,这种结构的成本也会上升。因此,我们需要将这些组件拆分成更小的模块,使用一个中间件来连接它们。 750 | 751 | 这种结构在现实世界中是非常常见的,我们的社会和经济体系中充满了中间件的机制,用以降低复杂度,压缩构建大型网络的成本。中间件也会被称为批发商、分包商、管理者等等。 752 | 753 | ZMQ网络也是一样,如果规模不断增长,就一定会需要中间件。ZMQ中,我们称其为“装置”。在构建ZMQ软件的初期,我们会画出几个节点,然后将它们连接起来,不使用中间件: 754 | 755 | ![6](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_6.png) 756 | 757 | 随后,我们对这个结构不断地进行扩充,将装置放到特定的位置,进一步增加节点数量: 758 | 759 | ![7](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_7.png) 760 | 761 | ZMQ装置没有具体的设计规则,但一般会有一组“前端”端点和一组“后端”端点。装置是无状态的,因此可以被广泛地部署在网络中。你可以在进程中启动一个线程来运行装置,或者直接在一个进程中运行装置。ZMQ内部也提供了基本的装置实现可供使用。 762 | 763 | ZMQ装置可以用作路由和寻址、提供服务、队列调度、以及其他你所能想到的事情。不同的消息模式需要用到不同类型的装置来构建网络。如,请求-应答模式中可以使用队列装置、抽象服务;发布-订阅模式中则可使用流装置、主题装置等。 764 | 765 | ZMQ装置比起其他中间件的优势在于,你可以将它放在网络中任何一个地方,完成任何你想要的事情。 766 | 767 | #### 发布-订阅代理服务 768 | 769 | 我们经常会需要将发布-订阅模式扩充到不同类型的网络中。比如说,有一组订阅者是在外网上的,我们想用广播的方式发布消息给内网的订阅者,而用TCP协议发送给外网订阅者。 770 | 771 | 我们要做的就是写一个简单的代理服务装置,在发布者和外网订阅者之间搭起桥梁。这个装置有两个端点,一端连接内网上的发布者,另一端连接到外网上。它会从发布者处接收订阅的消息,并转发给外网上的订阅者们。 772 | 773 | **wuproxy: Weather update proxy in C** 774 | 775 | ```c 776 | // 777 | // 气象信息代理服务装置 778 | // 779 | #include "zhelpers.h" 780 | 781 | int main (void) 782 | { 783 | void *context = zmq_init (1); 784 | 785 | // 订阅气象信息 786 | void *frontend = zmq_socket (context, ZMQ_SUB); 787 | zmq_connect (frontend, "tcp://192.168.55.210:5556"); 788 | 789 | // 转发气象信息 790 | void *backend = zmq_socket (context, ZMQ_PUB); 791 | zmq_bind (backend, "tcp://10.1.1.0:8100"); 792 | 793 | // 订阅所有消息 794 | zmq_setsockopt (frontend, ZMQ_SUBSCRIBE, "", 0); 795 | 796 | // 转发消息 797 | while (1) { 798 | while (1) { 799 | zmq_msg_t message; 800 | int64_t more; 801 | 802 | // 处理所有的消息帧 803 | zmq_msg_init (&message); 804 | zmq_recv (frontend, &message, 0); 805 | size_t more_size = sizeof (more); 806 | zmq_getsockopt (frontend, ZMQ_RCVMORE, &more, &more_size); 807 | zmq_send (backend, &message, more? ZMQ_SNDMORE: 0); 808 | zmq_msg_close (&message); 809 | if (!more) 810 | break; // 到达最后一帧 811 | } 812 | } 813 | // 程序不会运行到这里,但依然要正确地退出 814 | zmq_close (frontend); 815 | zmq_close (backend); 816 | zmq_term (context); 817 | return 0; 818 | } 819 | ``` 820 | 821 | 我们称这个装置为代理,因为它既是订阅者,又是发布者。这就意味着,添加该装置时不需要更改其他程序的代码,只需让外网订阅者知道新的网络地址即可。 822 | 823 | ![8](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_8.png) 824 | 825 | 可以注意到,这段程序能够正确处理多帧消息,会将它完整的转发给订阅者。如果我们在发送时不指定ZMQ_SNDMORE选项,那么下游节点收到的消息就可能是破损的。编写装置时应该要保证能够正确地处理多帧消息,否则会造成消息的丢失。 826 | 827 | #### 请求-应答代理 828 | 829 | 下面让我们在请求-应答模式中编写一个小型的消息队列代理装置。 830 | 831 | 在Hello World客户/服务模型中,一个客户端和一个服务端进行通信。但在真实环境中,我们会需要让多个客户端和多个服务端进行通信。关键问题在于,服务端应该是无状态的,所有的状态都应该包含在一次请求中,或者存放其它介质中,如数据库。 832 | 833 | 我们有两种方式来连接多个客户端和多个服务端。第一种是让客户端直接和多个服务端进行连接。客户端套接字可以连接至多个服务端套接字,它所发送的请求会通过负载均衡的方式分发给服务端。比如说,有一个客户端连接了三个服务端,A、B、C,客户端产生了R1、R2、R3、R4四个请求,那么,R1和R4会由服务A处理,R2由B处理,R3由C处理: 834 | 835 | ![9](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_9.png) 836 | 837 | 这种设计的好处在于可以方便地添加客户端,但若要添加服务端,那就得修改每个客户端的配置。如果你有100个客户端,需要添加三个服务端,那么这些客户端都需要重新进行配置,让其知道新服务端的存在。 838 | 839 | 这种方式肯定不是我们想要的。一个网络结构中如果有太多固化的模块就越不容易扩展。因此,我们需要有一个模块位于客户端和服务端之间,将所有的知识都汇聚到这个网络拓扑结构中。理想状态下,我们可以任意地增减客户端或是服务端,不需要更改任何组件的配置。 840 | 841 | 下面就让我们编写这样一个组件。这个代理会绑定到两个端点,前端端点供客户端连接,后端端点供服务端连接。它会使用zmq_poll()来轮询这两个套接字,接收消息并进行转发。装置中不会有队列的存在,因为ZMQ已经自动在套接字中完成了。 842 | 843 | 在使用REQ和REP套接字时,其请求-应答的会话是严格同步。客户端发送请求,服务端接收请求并发送应答,由客户端接收。如果客户端或服务端中的一个发生问题(如连续两次发送请求),程序就会报错。 844 | 845 | 但是,我们的代理装置必须要是非阻塞式的,虽然可以使用zmq_poll()同时处理两个套接字,但这里显然不能使用REP和REQ套接字。 846 | 847 | 幸运的是,我们有DEALER和ROUTER套接字可以胜任这项工作,进行非阻塞的消息收发。DEALER过去被称为XREQ,ROUTER被称为XREP,但新的代码中应尽量使用DEALER/ROUTER这种名称。在第三章中你会看到如何用DEALER和ROUTER套接字构建不同类型的请求-应答模式。 848 | 849 | 下面就让我们看看DEALER和ROUTER套接字是怎样在装置中工作的。 850 | 851 | 下方的简图描述了一个请求-应答模式,REQ和ROUTER通信,DEALER再和REP通信。ROUTER和DEALER之间我们则需要进行消息转发: 852 | 853 | ![10](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_10.png) 854 | 855 | 请求-应答代理会将两个套接字分别绑定到前端和后端,供客户端和服务端套接字连接。在使用该装置之前,还需要对客户端和服务端的代码进行调整。 856 | 857 | ** rrclient: Request-reply client in C ** 858 | 859 | ```c 860 | // 861 | // Hello world 客户端 862 | // 连接REQ套接字至 tcp://localhost:5559 端点 863 | // 发送Hello给服务端,等待World应答 864 | // 865 | #include "zhelpers.h" 866 | 867 | int main (void) 868 | { 869 | void *context = zmq_init (1); 870 | 871 | // 用于和服务端通信的套接字 872 | void *requester = zmq_socket (context, ZMQ_REQ); 873 | zmq_connect (requester, "tcp://localhost:5559"); 874 | 875 | int request_nbr; 876 | for (request_nbr = 0; request_nbr != 10; request_nbr++) { 877 | s_send (requester, "Hello"); 878 | char *string = s_recv (requester); 879 | printf ("收到应答 %d [%s]\n", request_nbr, string); 880 | free (string); 881 | } 882 | zmq_close (requester); 883 | zmq_term (context); 884 | return 0; 885 | } 886 | ``` 887 | 888 | 下面是服务代码: 889 | 890 | **rrserver: Request-reply service in C** 891 | 892 | ```c 893 | // 894 | // Hello World 服务端 895 | // 连接REP套接字至 tcp://*:5560 端点 896 | // 接收Hello请求,返回World应答 897 | // 898 | #include "zhelpers.h" 899 | 900 | int main (void) 901 | { 902 | void *context = zmq_init (1); 903 | 904 | // 用于何客户端通信的套接字 905 | void *responder = zmq_socket (context, ZMQ_REP); 906 | zmq_connect (responder, "tcp://localhost:5560"); 907 | 908 | while (1) { 909 | // 等待下一个请求 910 | char *string = s_recv (responder); 911 | printf ("Received request: [%s]\n", string); 912 | free (string); 913 | 914 | // 做一些“工作” 915 | sleep (1); 916 | 917 | // 返回应答信息 918 | s_send (responder, "World"); 919 | } 920 | // 程序不会运行到这里,不过还是做好清理工作 921 | zmq_close (responder); 922 | zmq_term (context); 923 | return 0; 924 | } 925 | ``` 926 | 927 | 最后是代理程序,可以看到它是能够处理多帧消息的: 928 | 929 | **rrbroker: Request-reply broker in C** 930 | 931 | ```c 932 | // 933 | // 简易请求-应答代理 934 | // 935 | #include "zhelpers.h" 936 | 937 | int main (void) 938 | { 939 | // 准备上下文和套接字 940 | void *context = zmq_init (1); 941 | void *frontend = zmq_socket (context, ZMQ_ROUTER); 942 | void *backend = zmq_socket (context, ZMQ_DEALER); 943 | zmq_bind (frontend, "tcp://*:5559"); 944 | zmq_bind (backend, "tcp://*:5560"); 945 | 946 | // 初始化轮询集合 947 | zmq_pollitem_t items [] = { 948 | { frontend, 0, ZMQ_POLLIN, 0 }, 949 | { backend, 0, ZMQ_POLLIN, 0 } 950 | }; 951 | // 在套接字间转发消息 952 | while (1) { 953 | zmq_msg_t message; 954 | int64_t more; // 检测多帧消息 955 | 956 | zmq_poll (items, 2, -1); 957 | if (items [0].revents & ZMQ_POLLIN) { 958 | while (1) { 959 | // 处理所有消息帧 960 | zmq_msg_init (&message); 961 | zmq_recv (frontend, &message, 0); 962 | size_t more_size = sizeof (more); 963 | zmq_getsockopt (frontend, ZMQ_RCVMORE, &more, &more_size); 964 | zmq_send (backend, &message, more? ZMQ_SNDMORE: 0); 965 | zmq_msg_close (&message); 966 | if (!more) 967 | break; // 最后一帧 968 | } 969 | } 970 | if (items [1].revents & ZMQ_POLLIN) { 971 | while (1) { 972 | // 处理所有消息帧 973 | zmq_msg_init (&message); 974 | zmq_recv (backend, &message, 0); 975 | size_t more_size = sizeof (more); 976 | zmq_getsockopt (backend, ZMQ_RCVMORE, &more, &more_size); 977 | zmq_send (frontend, &message, more? ZMQ_SNDMORE: 0); 978 | zmq_msg_close (&message); 979 | if (!more) 980 | break; // 最后一帧 981 | } 982 | } 983 | } 984 | // 程序不会运行到这里,不过还是做好清理工作 985 | zmq_close (frontend); 986 | zmq_close (backend); 987 | zmq_term (context); 988 | return 0; 989 | } 990 | ``` 991 | 992 | 使用请求-应答代理可以让你的C/S网络结构更易于扩展:客户端不知道服务端的存在,服务端不知道客户端的存在。网络中唯一稳定的组件是中间的代理装置: 993 | 994 | ![11](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_11.png) 995 | 996 | #### 内置装置 997 | 998 | ZMQ提供了一些内置的装置,不过大多数人需要自己手动编写这些装置。内置装置有: 999 | 1000 | * QUEUE,可用作请求-应答代理; 1001 | * FORWARDER,可用作发布-订阅代理服务; 1002 | * STREAMER,可用作管道模式代理。 1003 | 1004 | 可以使用zmq_device()来启动一个装置,需要传递两个套接字给它: 1005 | 1006 | ```c 1007 | zmq_device (ZMQ_QUEUE, frontend, backend); 1008 | ``` 1009 | 1010 | 启动了QUEUE队列就如同在网络中加入了一个请求-应答代理,只需为其创建已绑定或连接的套接字即可。下面这段代码是使用内置装置的情形: 1011 | 1012 | **msgqueue: Message queue broker in C** 1013 | 1014 | ```c 1015 | // 1016 | // 简单消息队列代理 1017 | // 功能和请求-应答代理相同,但使用了内置的装置 1018 | // 1019 | #include "zhelpers.h" 1020 | 1021 | int main (void) 1022 | { 1023 | void *context = zmq_init (1); 1024 | 1025 | // 客户端套接字 1026 | void *frontend = zmq_socket (context, ZMQ_ROUTER); 1027 | zmq_bind (frontend, "tcp://*:5559"); 1028 | 1029 | // 服务端套接字 1030 | void *backend = zmq_socket (context, ZMQ_DEALER); 1031 | zmq_bind (backend, "tcp://*:5560"); 1032 | 1033 | // 启动内置装置 1034 | zmq_device (ZMQ_QUEUE, frontend, backend); 1035 | 1036 | // 程序不会运行到这里 1037 | zmq_close (frontend); 1038 | zmq_close (backend); 1039 | zmq_term (context); 1040 | return 0; 1041 | } 1042 | ``` 1043 | 1044 | 内置装置会恰当地处理错误,而我们手工实现的代理并没有加入错误处理机制。所以说,当你能够在程序中使用内置装置的时候就尽量用吧。 1045 | 1046 | 可能你会像某些ZMQ开发者一样提出这样一个问题:如果我将其他类型的套接字传入这些装置中会发生什么?答案是:别这么做。你可以随意传入不同类型的套接字,但是执行结果会非常奇怪。所以,QUEUE装置应使用ROUTER/DEALER套接字、FORWARDER应使用SUB/PUB、STREAMER应使用PULL/PUSH。 1047 | 1048 | 当你需要其他的套接字类型进行组合时,那就需要自己编写装置了。 1049 | 1050 | ### ZMQ多线程编程 1051 | 1052 | 使用ZMQ进行多线程编程(MT编程)将会是一种享受。在多线程中使用ZMQ套接字时,你不需要考虑额外的东西,让它们自如地运作就好。 1053 | 1054 | 使用ZMQ进行多线程编程时,**不需要考虑互斥、锁、或其他并发程序中要考虑的因素,你唯一要关心的仅仅是线程之间的消息**。 1055 | 1056 | 什么叫“完美”的多线程编程,指的是代码易写易读,可以跨系统、跨语言地使用同一种技术,能够在任意颗核心的计算机上运行,没有状态,没有速度的瓶颈。 1057 | 1058 | 如果你有多年的多线程编程经验,知道如何使用锁、信号灯、临界区等机制来使代码运行得正确(尚未考虑快速),那你可能会很沮丧,因为ZMQ将改变这一切。三十多年来,并发式应用程序开发所总结的经验是:不要共享状态。这就好比两个醉汉想要分享一杯啤酒,如果他们不是铁哥们儿,那他们很快就会打起来。当有更多的醉汉加入时,情况就会更糟。多线程编程有时就像醉汉抢夺啤酒那样糟糕。 1059 | 1060 | 进行多线程编程往往是痛苦的,当程序因为压力过大而崩溃时,你会不知所然。有人写过一篇《多线程代码中的11个错误易发点》的文章,在大公司中广为流传,列举其中的几项:没有进行同步、错误的粒度、读写分离、无锁排序、锁传递、优先级冲突等。 1061 | 1062 | 假设某一天的下午三点,当证券市场正交易得如火如荼的时候,突然之间,应用程序因为锁的问题崩溃了,那将会是何等的场景?所以,作为程序员的我们,为解决那些复杂的多线程问题,只能用上更复杂的编程机制。 1063 | 1064 | 有人曾这样比喻,那些多线程程序原本应作为大型公司的核心支柱,但往往又最容易出错;那些想要通过网络不断进行延伸的产品,最后总以失败告终。 1065 | 1066 | 如何用ZMQ进行多线程编程,以下是一些规则: 1067 | 1068 | * 不要在不同的线程之间访问同一份数据,如果要用到传统编程中的互斥机制,那就有违ZMQ的思想了。唯一的例外是ZMQ上下文对象,它是线程安全的。 1069 | 1070 | * 必须为进程创建ZMQ上下文,并将其传递给所有你需要使用inproc协议进行通信的线程; 1071 | 1072 | * 你可以将线程作为单独的任务来对待,使用自己的上下文,但是这些线程之间就不能使用inproc协议进行通信了。这样做的好处是可以在日后方便地将程序拆分为不同的进程来运行。 1073 | 1074 | * 不要在不同的线程之间传递套接字对象,这些对象不是线程安全的。从技术上来说,你是可以这样做的,但是会用到互斥和锁的机制,这会让你的应用程序变得缓慢和脆弱。唯一合理的情形是,在某些语言的ZMQ类库内部,需要使用垃圾回收机制,这时可能会进行套接字对象的传递。 1075 | 1076 | 当你需要在应用程序中使用两个装置时,可能会将套接字对象从一个线程传递给另一个线程,这样做一开始可能会成功,但最后一定会随机地发生错误。所以说,应在同一个线程中打开和关闭套接字。 1077 | 1078 | 如果你能遵循上面的规则,就会发现多线程程序可以很容易地拆分成多个进程。程序逻辑可以在线程、进程、或是计算机中运行,根据你的需求进行部署即可。 1079 | 1080 | ZMQ使用的是系统原生的线程机制,而不是某种“绿色线程”。这样做的好处是你不需要学习新的多线程编程API,而且可以和目标操作系统进行很好的结合。你可以使用类似英特尔的ThreadChecker工具来查看线程工作的情况。缺点在于,如果程序创建了太多的线程(如上千个),则可能导致操作系统负载过高。 1081 | 1082 | 下面我们举一个实例,让原来的Hello World服务变得更为强大。原来的服务是单线程的,如果请求较少,自然没有问题。ZMQ的线程可以在一个核心上高速地运行,执行大量的工作。但是,如果有一万次请求同时发送过来会怎么样?因此,现实环境中,我们会启动多个worker线程,他们会尽可能地接收客户端请求,处理并返回应答。 1083 | 1084 | 当然,我们可以使用启动多个worker进程的方式来实现,但是启动一个进程总比启动多个进程要来的方便且易于管理。而且,作为线程启动的worker,所占用的带宽会比较少,延迟也会较低。 1085 | 以下是多线程版的Hello World服务: 1086 | 1087 | **mtserver: Multithreaded service in C** 1088 | 1089 | ```c 1090 | // 1091 | // 多线程版Hello World服务 1092 | // 1093 | #include "zhelpers.h" 1094 | #include 1095 | 1096 | static void * 1097 | worker_routine (void *context) { 1098 | // 连接至代理的套接字 1099 | void *receiver = zmq_socket (context, ZMQ_REP); 1100 | zmq_connect (receiver, "inproc://workers"); 1101 | 1102 | while (1) { 1103 | char *string = s_recv (receiver); 1104 | printf ("Received request: [%s]\n", string); 1105 | free (string); 1106 | // 工作 1107 | sleep (1); 1108 | // 返回应答 1109 | s_send (receiver, "World"); 1110 | } 1111 | zmq_close (receiver); 1112 | return NULL; 1113 | } 1114 | 1115 | int main (void) 1116 | { 1117 | void *context = zmq_init (1); 1118 | 1119 | // 用于和client进行通信的套接字 1120 | void *clients = zmq_socket (context, ZMQ_ROUTER); 1121 | zmq_bind (clients, "tcp://*:5555"); 1122 | 1123 | // 用于和worker进行通信的套接字 1124 | void *workers = zmq_socket (context, ZMQ_DEALER); 1125 | zmq_bind (workers, "inproc://workers"); 1126 | 1127 | // 启动一个worker池 1128 | int thread_nbr; 1129 | for (thread_nbr = 0; thread_nbr < 5; thread_nbr++) { 1130 | pthread_t worker; 1131 | pthread_create (&worker, NULL, worker_routine, context); 1132 | } 1133 | // 启动队列装置 1134 | zmq_device (ZMQ_QUEUE, clients, workers); 1135 | 1136 | // 程序不会运行到这里,但仍进行清理工作 1137 | zmq_close (clients); 1138 | zmq_close (workers); 1139 | zmq_term (context); 1140 | return 0; 1141 | } 1142 | ``` 1143 | 1144 | 所有的代码应该都已经很熟悉了: 1145 | 1146 | * 服务端启动一组worker线程,每个worker创建一个REP套接字,并处理收到的请求。worker线程就像是一个单线程的服务,唯一的区别是使用了inproc而非tcp协议,以及绑定-连接的方向调换了。 1147 | * 服务端创建ROUTER套接字用以和client通信,因此提供了一个TCP协议的外部接口。 1148 | * 服务端创建DEALER套接字用以和worker通信,使用了内部接口(inproc)。 1149 | * 服务端启动了QUEUE内部装置,连接两个端点上的套接字。QUEUE装置会将收到的请求分发给连接上的worker,并将应答路由给请求方。 1150 | 1151 | 需要注意的是,在某些编程语言中,创建线程并不是特别方便,POSIX提供的类库是pthreads,但Windows中就需要使用不同的API了。我们会在第三章中讲述如何包装一个多线程编程的API。 1152 | 1153 | 示例中的“工作”仅仅是1秒钟的停留,我们可以在worker中进行任意的操作,包括与其他节点进行通信。消息的流向是这样的:REQ-ROUTER-queue-DEALER-REP。 1154 | 1155 | ![12](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_12.png) 1156 | 1157 | ### 线程间的信号传输 1158 | 1159 | 当你刚开始使用ZMQ进行多线程编程时,你可能会问:要如何协调两个线程的工作呢?可能会想要使用sleep()这样的方法,或者使用诸如信号、互斥等机制。事实上,**你唯一要用的就是ZMQ本身**。回忆一下那个醉汉抢啤酒的例子吧。 1160 | 1161 | 下面的示例演示了三个线程之间需要如何进行同步: 1162 | 1163 | ![13](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_13.png) 1164 | 1165 | 我们使用PAIR套接字和inproc协议。 1166 | 1167 | **mtrelay: Multithreaded relay in C** 1168 | 1169 | ```c 1170 | // 1171 | // 多线程同步 1172 | // 1173 | #include "zhelpers.h" 1174 | #include 1175 | 1176 | static void * 1177 | step1 (void *context) { 1178 | // 连接至步骤2,告知我已就绪 1179 | void *xmitter = zmq_socket (context, ZMQ_PAIR); 1180 | zmq_connect (xmitter, "inproc://step2"); 1181 | printf ("步骤1就绪,正在通知步骤2……\n"); 1182 | s_send (xmitter, "READY"); 1183 | zmq_close (xmitter); 1184 | 1185 | return NULL; 1186 | } 1187 | 1188 | static void * 1189 | step2 (void *context) { 1190 | // 启动步骤1前线绑定至inproc套接字 1191 | void *receiver = zmq_socket (context, ZMQ_PAIR); 1192 | zmq_bind (receiver, "inproc://step2"); 1193 | pthread_t thread; 1194 | pthread_create (&thread, NULL, step1, context); 1195 | 1196 | // 等待信号 1197 | char *string = s_recv (receiver); 1198 | free (string); 1199 | zmq_close (receiver); 1200 | 1201 | // 连接至步骤3,告知我已就绪 1202 | void *xmitter = zmq_socket (context, ZMQ_PAIR); 1203 | zmq_connect (xmitter, "inproc://step3"); 1204 | printf ("步骤2就绪,正在通知步骤3……\n"); 1205 | s_send (xmitter, "READY"); 1206 | zmq_close (xmitter); 1207 | 1208 | return NULL; 1209 | } 1210 | 1211 | int main (void) 1212 | { 1213 | void *context = zmq_init (1); 1214 | 1215 | // 启动步骤2前线绑定至inproc套接字 1216 | void *receiver = zmq_socket (context, ZMQ_PAIR); 1217 | zmq_bind (receiver, "inproc://step3"); 1218 | pthread_t thread; 1219 | pthread_create (&thread, NULL, step2, context); 1220 | 1221 | // 等待信号 1222 | char *string = s_recv (receiver); 1223 | free (string); 1224 | zmq_close (receiver); 1225 | 1226 | printf ("测试成功!\n"); 1227 | zmq_term (context); 1228 | return 0; 1229 | } 1230 | ``` 1231 | 1232 | 这是一个ZMQ多线程编程的典型示例: 1233 | 1234 | 1. 两个线程通过inproc协议进行通信,使用同一个上下文; 1235 | 1. 父线程创建一个套接字,绑定至inproc://端点,然后再启动子线程,将上下文对象传递给它; 1236 | 1. 子线程创建第二个套接字,连接至inproc://端点,然后发送已就绪信号给父线程。 1237 | 1238 | 需要注意的是,这段代码无法扩展到多个进程之间的协调。如果你使用inproc协议,只能建立结构非常紧密的应用程序。在延迟时间必须严格控制的情况下可以使用这种方法。对其他应用程序来说,每个线程使用同一个上下文,协议选用ipc或tcp。然后,你就可以自由地将应用程序拆分为多个进程甚至是多台计算机了。 1239 | 1240 | 这是我们第一次使用PAIR套接字。为什么要使用PAIR?其他类型的套接字也可以使用,但都有一些缺点会影响到线程间的通信: 1241 | 1242 | * 你可以让信号发送方使用PUSH,接收方使用PULL,这看上去可能可以,但是需要注意的是,PUSH套接字发送消息时会进行负载均衡,如果你不小心开启了两个接收方,就会“丢失”一半的信号。而PAIR套接字建立的是一对一的连接,具有排他性。 1243 | 1244 | * 可以让发送方使用DEALER,接收方使用ROUTER。但是,ROUTER套接字会在消息的外层包裹一个来源地址,这样一来原本零字节的信号就可能要成为一个多段消息了。如果你不在乎这个问题,并且不会重复读取那个套接字,自然可以使用这种方法。但是,如果你想要使用这个套接字接收真正的数据,你就会发现ROUTER提供的消息是错误的。至于DEALER套接字,它同样有负载均衡的机制,和PUSH套接字有相同的风险。 1245 | 1246 | * 可以让发送方使用PUB,接收方使用SUB。一来消息可以照原样发送,二来PUB套接字不会进行负载均衡。但是,你需要对SUB套接字设置一个空的订阅信息(用以接收所有消息);而且,如果SUB套接字没有及时和PUB建立连接,消息很有可能会丢失。 1247 | 1248 | 综上,使用PAIR套接字进行线程间的协调是最合适的。 1249 | 1250 | ### 节点协调 1251 | 1252 | 当你想要对节点进行协调时,PAIR套接字就不怎么合适了,这也是线程和节点之间的不同之处。一般来说,节点是来去自由的,而线程则较为稳定。使用PAIR套接字时,若远程节点断开连接后又进行重连,PAIR不会予以理会。 1253 | 1254 | 第二个区别在于,线程的数量一般是固定的,而节点数量则会经常变化。让我们以气象信息模型为基础,看看要怎样进行节点的协调,以保证客户端不会丢失最开始的那些消息。 1255 | 1256 | 下面是程序运行逻辑: 1257 | 1258 | * 发布者知道预期的订阅者数量,这个数字可以任意指定; 1259 | * 发布者启动后会先等待所有订阅者进行连接,也就是节点协调。每个订阅者会使用另一个套接字来告知发布者自己已就绪; 1260 | * 当所有订阅者准备就绪后,发布者才开始发送消息。 1261 | 1262 | 这里我们会使用REQ-REP套接字来同步发布者和订阅者。发布者的代码如下: 1263 | 1264 | **syncpub: Synchronized publisher in C** 1265 | 1266 | ```c 1267 | // 1268 | // 发布者 - 同步版 1269 | // 1270 | #include "zhelpers.h" 1271 | 1272 | // 等待10个订阅者连接 1273 | #define SUBSCRIBERS_EXPECTED 10 1274 | 1275 | int main (void) 1276 | { 1277 | void *context = zmq_init (1); 1278 | 1279 | // 用于和客户端通信的套接字 1280 | void *publisher = zmq_socket (context, ZMQ_PUB); 1281 | zmq_bind (publisher, "tcp://*:5561"); 1282 | 1283 | // 用于接收信号的套接字 1284 | void *syncservice = zmq_socket (context, ZMQ_REP); 1285 | zmq_bind (syncservice, "tcp://*:5562"); 1286 | 1287 | // 接收订阅者的就绪信号 1288 | printf ("正在等待订阅者就绪\n"); 1289 | int subscribers = 0; 1290 | while (subscribers < SUBSCRIBERS_EXPECTED) { 1291 | // - 等待就绪信息 1292 | char *string = s_recv (syncservice); 1293 | free (string); 1294 | // - 发送应答 1295 | s_send (syncservice, ""); 1296 | subscribers++; 1297 | } 1298 | // 开始发送100万条数据 1299 | printf ("正在广播消息\n"); 1300 | int update_nbr; 1301 | for (update_nbr = 0; update_nbr < 1000000; update_nbr++) 1302 | s_send (publisher, "Rhubarb"); 1303 | 1304 | s_send (publisher, "END"); 1305 | 1306 | zmq_close (publisher); 1307 | zmq_close (syncservice); 1308 | zmq_term (context); 1309 | return 0; 1310 | } 1311 | ``` 1312 | 1313 | ![14](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_14.png) 1314 | 1315 | 以下是订阅者的代码: 1316 | 1317 | **syncsub: Synchronized subscriber in C** 1318 | 1319 | ```c 1320 | // 1321 | // 订阅者 - 同步版 1322 | // 1323 | #include "zhelpers.h" 1324 | 1325 | int main (void) 1326 | { 1327 | void *context = zmq_init (1); 1328 | 1329 | // 一、连接SUB套接字 1330 | void *subscriber = zmq_socket (context, ZMQ_SUB); 1331 | zmq_connect (subscriber, "tcp://localhost:5561"); 1332 | zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "", 0); 1333 | 1334 | // ZMQ太快了,我们延迟一会儿…… 1335 | sleep (1); 1336 | 1337 | // 二、与发布者进行同步 1338 | void *syncclient = zmq_socket (context, ZMQ_REQ); 1339 | zmq_connect (syncclient, "tcp://localhost:5562"); 1340 | 1341 | // - 发送请求 1342 | s_send (syncclient, ""); 1343 | 1344 | // - 等待应答 1345 | char *string = s_recv (syncclient); 1346 | free (string); 1347 | 1348 | // 三、处理消息 1349 | int update_nbr = 0; 1350 | while (1) { 1351 | char *string = s_recv (subscriber); 1352 | if (strcmp (string, "END") == 0) { 1353 | free (string); 1354 | break; 1355 | } 1356 | free (string); 1357 | update_nbr++; 1358 | } 1359 | printf ("收到 %d 条消息\n", update_nbr); 1360 | 1361 | zmq_close (subscriber); 1362 | zmq_close (syncclient); 1363 | zmq_term (context); 1364 | return 0; 1365 | } 1366 | ``` 1367 | 1368 | 以下这段shell脚本会启动10个订阅者、1个发布者: 1369 | 1370 | ```sh 1371 | echo "正在启动订阅者..." 1372 | for a in 1 2 3 4 5 6 7 8 9 10; do 1373 | syncsub & 1374 | done 1375 | echo "正在启动发布者..." 1376 | syncpub 1377 | ``` 1378 | 1379 | 结果如下: 1380 | 1381 | ``` 1382 | 正在启动订阅者... 1383 | 正在启动发布者... 1384 | 收到 1000000 条消息 1385 | 收到 1000000 条消息 1386 | 收到 1000000 条消息 1387 | 收到 1000000 条消息 1388 | 收到 1000000 条消息 1389 | 收到 1000000 条消息 1390 | 收到 1000000 条消息 1391 | 收到 1000000 条消息 1392 | 收到 1000000 条消息 1393 | 收到 1000000 条消息 1394 | ``` 1395 | 1396 | 当REQ-REP请求完成时,我们仍无法保证SUB套接字已成功建立连接。除非使用inproc协议,否则对外连接的顺序是不一定的。因此,示例程序中使用了sleep(1)的方式来进行处理,随后再发送同步请求。 1397 | 1398 | 更可靠的模型可以是: 1399 | 1400 | * 发布者打开PUB套接字,开始发送Hello消息(非数据); 1401 | * 订阅者连接SUB套接字,当收到Hello消息后再使用REQ-REP套接字进行同步; 1402 | * 当发布者获得所有订阅者的同步消息后,才开始发送真正的数据。 1403 | 1404 | ### 零拷贝 1405 | 1406 | 第一章中我们曾提过零拷贝是很危险的,其实那是吓唬你的。既然你已经读到这里了,说明你已经具备了足够的知识,能够使用零拷贝。但需要记住,条条大路通地狱,过早地对程序进行优化其实是没有必要的。简单的说,如果你用不好零拷贝,那可能会让程序架构变得更糟。 1407 | 1408 | ZMQ提供的API可以让你直接发送和接收消息,不用考虑缓存的问题。正因为消息是由ZMQ在后台收发的,所以使用零拷贝需要一些额外的工作。 1409 | 1410 | 做零拷贝时,使用zmq_msg_init_data()函数创建一条消息,其内容指向某个已经分配好的内存区域,然后将该消息传递给zmq_send()函数。创建消息时,你还需要提供一个用于释放消息内容的函数,ZMQ会在消息发送完毕时调用。下面是一个简单的例子,我们假设已经分配好的内存区域为1000个字节: 1411 | 1412 | ```c 1413 | void my_free (void *data, void *hint) { 1414 | free (data); 1415 | } 1416 | // Send message from buffer, which we allocate and 0MQ will free for us 1417 | zmq_msg_t message; 1418 | zmq_msg_init_data (&message, buffer, 1000, my_free, NULL); 1419 | zmq_send (socket, &message, 0); 1420 | ``` 1421 | 1422 | 在接收消息的时候是无法使用零拷贝的:ZMQ会将收到的消息放入一块内存区域供你读取,但不会将消息写入程序指定的内存区域。 1423 | 1424 | ZMQ的多段消息能够很好地支持零拷贝。在传统消息系统中,你需要将不同缓存中的内容保存到同一个缓存中,然后才能发送。但ZMQ会将来自不同内存区域的内容作为消息的一个帧进行发送。而且在ZMQ内部,一条消息会作为一个整体进行收发,因而非常高效。 1425 | 1426 | ### 瞬时套接字和持久套接字 1427 | 1428 | 在传统网络编程中,套接字是一个API对象,它们的生命周期不会长过程序的生命周期。但仔细打量一下套接字,它会占用一项特定的资源——缓存,这时ZMQ的开发者可能会问:是否有办法在程序崩溃时让这些套接字缓存得以保留,稍后能够恢复? 1429 | 1430 | 这种特性应该会非常有用,虽然不能应对所有的危险,但至少可以挽回一部分损失,特别是多发布-订阅模式来说。让我们来讨论一下。 1431 | 1432 | 这里有两个套接字正在欢快地传送着气象信息: 1433 | 1434 | ![15](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_15.png) 1435 | 1436 | 如果接收方(SUB、PULL、REQ)指定了套接字标识,当它们断开网络时,发送方(PUB、PUSH、REP)会为它们缓存信息,直至达到阈值(HWM)。这里发送方不需要有套接字标识。 1437 | 1438 | 需要注意,ZMQ的套接字缓存对程序员来说是不可见的,正如TCP缓存一样。 1439 | 1440 | 到目前为止,我们使用的套接字都是瞬时套接字。要将瞬时套接字转化为持久套接字,需要为其设定一个套接字标识。所有的ZMQ套接字都会有一个标识,不过是由ZMQ自动生成的UUID。 1441 | 1442 | 在ZMQ内部,两个套接字相连时会先交换各自的标识。如果发生对方没有ID,则会自行生成一个用以标识对方: 1443 | 1444 | ![16](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_16.png) 1445 | 1446 | 但套接字也可以告知对方自己的标识,那当它们第二次连接时,就能知道对方的身份: 1447 | 1448 | ``` 1449 | +-----------+ 1450 | | | 1451 | | Sender | 1452 | | | 1453 | +-----------+ 1454 | | Socket | 1455 | \-----------/ 1456 | ^ "Lucy! Nice to see you again..." 1457 | | 1458 | | 1459 | | "My name's Lucy" 1460 | /-----+-----\ 1461 | | Socket | 1462 | +-----------+ 1463 | | | 1464 | | Receiver | 1465 | | | 1466 | +-----------+ 1467 | 1468 | 1469 | Figure # - Durable socket 1470 | ``` 1471 | 1472 | 下面这行代码就可以为套接字设置标识,从而建立了一个持久的套接字: 1473 | 1474 | ```c 1475 | zmq_setsockopt (socket, ZMQ_IDENTITY, "Lucy", 4); 1476 | ``` 1477 | 1478 | 关于套接字标识还有几点说明: 1479 | 1480 | * 如果要为套接字设置标识,必须在连接或绑定至端点之前设置; 1481 | * 接收方会选择使用套接字标识,正如cookie在HTTP网页应用中的性质,是由服务器去选择要使用哪个cookie的; 1482 | * 套接字标识是二进制字符串;以字节0开头的套接字标识为ZMQ保留标识; 1483 | * 不用为多个套接字指定相同的标识,若套接字使用的标识已被占用,它将无法连接至其他套接字; 1484 | * 不要使用随机的套接字标识,这样会生成很多持久化套接字,最终让节点崩溃; 1485 | * 如果你想获取对方套接字的标识,只有ROUTER套接字会帮你自动完成这件事,使用其他套接字类型时,需要将标识作为消息的一帧发送过来; 1486 | * 说了以上这些,使用持久化套接字其实并不明智,因为它会让发送者越来越混乱,让架构变得脆弱。如果我们能重新设计ZMQ,很可能会去掉这种显式声明套接字标识的功能。 1487 | 1488 | 其他信息可以查看zmq_setsockopt()函数的ZMQ_IDENTITY一节。注意,该方法只能获取程序中套接字的标识,而不能获得对方套接字的标识。 1489 | 1490 | ### 发布-订阅消息信封 1491 | 1492 | 我们简单介绍了多帧消息,下面就来看看它的典型用法——消息信封。信封是指为消息注明来源地址,而不修改消息内容。 1493 | 1494 | 在发布-订阅模式中,信封包含了订阅信息,用以过滤掉不需要接收的消息。 1495 | 1496 | 如果你想要使用发布-订阅信封,就需要自行生成和设置。这个动作是可选的,我们在之前的示例中也没有使用到。在发布-订阅模式中使用信封可能会比较麻烦,但在现实应用中还是很有必要的,毕竟信封和消息的确是两块不想干的数据。 1497 | 1498 | 这是发布-订阅模式中一个带有信封的消息: 1499 | 1500 | ![17](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_17.png) 1501 | 1502 | 我们回忆一下,发布-订阅模式中,消息的接收是根据订阅信息来的,也就是消息的前缀。将这个前缀放入单独的消息帧,可以让匹配变得非常明显。因为不会有一个应用程序恰好只匹配了一部分数据。 1503 | 1504 | 下面是一个最简的发布-订阅消息信封示例。发布者会发送两类消息:A和B,信封中指明了消息类型: 1505 | 1506 | **psenvpub: Pub-sub envelope publisher in C** 1507 | 1508 | ```c 1509 | // 1510 | // 发布-订阅消息信封 - 发布者 1511 | // s_sendmore()函数也是zhelpers.h提供的 1512 | // 1513 | #include "zhelpers.h" 1514 | 1515 | int main (void) 1516 | { 1517 | // 准备上下文和PUB套接字 1518 | void *context = zmq_init (1); 1519 | void *publisher = zmq_socket (context, ZMQ_PUB); 1520 | zmq_bind (publisher, "tcp://*:5563"); 1521 | 1522 | while (1) { 1523 | // 发布两条消息,A类型和B类型 1524 | s_sendmore (publisher, "A"); 1525 | s_send (publisher, "We don't want to see this"); 1526 | s_sendmore (publisher, "B"); 1527 | s_send (publisher, "We would like to see this"); 1528 | sleep (1); 1529 | } 1530 | // 正确退出 1531 | zmq_close (publisher); 1532 | zmq_term (context); 1533 | return 0; 1534 | } 1535 | ``` 1536 | 1537 | 假设订阅者只需要B类型的消息: 1538 | 1539 | **psenvsub: Pub-sub envelope subscriber in C** 1540 | 1541 | ```c 1542 | // 1543 | // 发布-订阅消息信封 - 订阅者 1544 | // 1545 | #include "zhelpers.h" 1546 | 1547 | int main (void) 1548 | { 1549 | // 准备上下文和SUB套接字 1550 | void *context = zmq_init (1); 1551 | void *subscriber = zmq_socket (context, ZMQ_SUB); 1552 | zmq_connect (subscriber, "tcp://localhost:5563"); 1553 | zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "B", 1); 1554 | 1555 | while (1) { 1556 | // 读取消息信封 1557 | char *address = s_recv (subscriber); 1558 | // 读取消息内容 1559 | char *contents = s_recv (subscriber); 1560 | printf ("[%s] %s\n", address, contents); 1561 | free (address); 1562 | free (contents); 1563 | } 1564 | // 正确退出 1565 | zmq_close (subscriber); 1566 | zmq_term (context); 1567 | return 0; 1568 | } 1569 | ``` 1570 | 1571 | 执行上面的程序时,订阅者会打印如下信息: 1572 | 1573 | ``` 1574 | [B] We would like to see this 1575 | [B] We would like to see this 1576 | [B] We would like to see this 1577 | [B] We would like to see this 1578 | ... 1579 | ``` 1580 | 1581 | 这个示例说明订阅者会丢弃未订阅的消息,且接收完整的多帧消息——你不会只获得消息的一部分。 1582 | 1583 | 如果你订阅了多个套接字,又想知道这些套接字的标识,从而通过另一个套接字来发送消息给它们(这个用例很常见),你可以让发布者创建一条含有三帧的消息: 1584 | 1585 | ![18](https://github.com/anjuke/zguide-cn/raw/master/images/chapter2_18.png) 1586 | 1587 | ### (半)持久订阅者和阈值(HWM) 1588 | 1589 | 所有的套接字类型都可以使用标识。如果你在使用PUB和SUB套接字,其中SUB套接字为自己声明了标识,那么,当SUB断开连接时,PUB会保留要发送给SUB的消息。 1590 | 1591 | 这种机制有好有坏。好的地方在于发布者会暂存这些消息,当订阅者重连后进行发送;不好的地方在于这样很容易让发布者因内存溢出而崩溃。 1592 | 1593 | **如果你在使用持久化的SUB套接字(即为SUB设置了套接字标识),那么你必须设法避免消息在发布者队列中堆砌并溢出,应该使用阈值(HWM)来保护发布者套接字**。发布者的阈值会分别影响所有的订阅者。 1594 | 1595 | 我们可以运行一个示例来证明这一点,用第一章中的wuclient和wuserver具体,在wuclient中进行套接字连接前加入这一行: 1596 | 1597 | ```c 1598 | zmq_setsockopt (subscriber, ZMQ_IDENTITY, "Hello", 5); 1599 | ``` 1600 | 1601 | 编译并运行这两段程序,一切看起来都很平常。但是观察一下发布者的内存占用情况,可以看到当订阅者逐个退出后,发布者的内存占用会逐渐上升。若此时你重启订阅者,会发现发布者的内存占用不再增长了,一旦订阅者停止,就又会增长。很快地,它就会耗尽系统资源。 1602 | 1603 | 我们先来看看如何设置阈值,然后再看如何设置得正确。下面的发布者和订阅者使用了上文提到的“节点协调”机制。发布者会每隔一秒发送一条消息,这时你可以中断订阅者,重新启动它,看看会发生什么。 1604 | 1605 | 以下是发布者的代码: 1606 | 1607 | **durapub: Durable publisher in C** 1608 | 1609 | ```c 1610 | // 1611 | // 发布者 - 连接持久化的订阅者 1612 | // 1613 | #include "zhelpers.h" 1614 | 1615 | int main (void) 1616 | { 1617 | void *context = zmq_init (1); 1618 | 1619 | // 订阅者会发送已就绪的消息 1620 | void *sync = zmq_socket (context, ZMQ_PULL); 1621 | zmq_bind (sync, "tcp://*:5564"); 1622 | 1623 | // 使用该套接字发布消息 1624 | void *publisher = zmq_socket (context, ZMQ_PUB); 1625 | zmq_bind (publisher, "tcp://*:5565"); 1626 | 1627 | // 等待同步消息 1628 | char *string = s_recv (sync); 1629 | free (string); 1630 | 1631 | // 广播10条消息,一秒一条 1632 | int update_nbr; 1633 | for (update_nbr = 0; update_nbr < 10; update_nbr++) { 1634 | char string [20]; 1635 | sprintf (string, "Update %d", update_nbr); 1636 | s_send (publisher, string); 1637 | sleep (1); 1638 | } 1639 | s_send (publisher, "END"); 1640 | 1641 | zmq_close (sync); 1642 | zmq_close (publisher); 1643 | zmq_term (context); 1644 | return 0; 1645 | } 1646 | ``` 1647 | 1648 | 下面是订阅者的代码: 1649 | 1650 | **durasub: Durable subscriber in C** 1651 | 1652 | ```c 1653 | // 1654 | // 持久化的订阅者 1655 | // 1656 | #include "zhelpers.h" 1657 | 1658 | int main (void) 1659 | { 1660 | void *context = zmq_init (1); 1661 | 1662 | // 连接SUB套接字 1663 | void *subscriber = zmq_socket (context, ZMQ_SUB); 1664 | zmq_setsockopt (subscriber, ZMQ_IDENTITY, "Hello", 5); 1665 | zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "", 0); 1666 | zmq_connect (subscriber, "tcp://localhost:5565"); 1667 | 1668 | // 发送同步消息 1669 | void *sync = zmq_socket (context, ZMQ_PUSH); 1670 | zmq_connect (sync, "tcp://localhost:5564"); 1671 | s_send (sync, ""); 1672 | 1673 | // 获取更新,并按指令退出 1674 | while (1) { 1675 | char *string = s_recv (subscriber); 1676 | printf ("%s\n", string); 1677 | if (strcmp (string, "END") == 0) { 1678 | free (string); 1679 | break; 1680 | } 1681 | free (string); 1682 | } 1683 | zmq_close (sync); 1684 | zmq_close (subscriber); 1685 | zmq_term (context); 1686 | return 0; 1687 | } 1688 | ``` 1689 | 1690 | 运行以上代码,在不同的窗口中先后打开发布者和订阅者。当订阅者获取了一至两条消息后按Ctrl-C中止,然后重新启动,看看执行结果: 1691 | 1692 | ``` 1693 | $ durasub 1694 | Update 0 1695 | Update 1 1696 | Update 2 1697 | ^C 1698 | $ durasub 1699 | Update 3 1700 | Update 4 1701 | Update 5 1702 | Update 6 1703 | Update 7 1704 | ^C 1705 | $ durasub 1706 | Update 8 1707 | Update 9 1708 | END 1709 | ``` 1710 | 1711 | 可以看到订阅者的唯一区别是为套接字设置了标识,发布者就会将消息缓存起来,待重建连接后发送。设置套接字标识可以让瞬时套接字转变为持久套接字。实践中,你需要小心地给套接字起名字,可以从配置文件中获取,或者生成一个UUID并保存起来。 1712 | 1713 | 当我们为PUB套接字设置了阈值,发布者就会缓存指定数量的消息,转而丢弃溢出的消息。让我们将阈值设置为2,看看会发生什么: 1714 | 1715 | ```c 1716 | uint64_t hwm = 2; 1717 | zmq_setsockopt (publisher, ZMQ_HWM, &hwm, sizeof (hwm)); 1718 | ``` 1719 | 1720 | 运行程序,中断订阅者后等待一段时间再重启,可以看到结果如下: 1721 | 1722 | ``` 1723 | $ durasub 1724 | Update 0 1725 | Update 1 1726 | ^C 1727 | $ durasub 1728 | Update 2 1729 | Update 3 1730 | Update 7 1731 | Update 8 1732 | Update 9 1733 | END 1734 | ``` 1735 | 1736 | 看仔细了,发布者只为我们保存了两条消息(2和3)。阈值使得ZMQ丢弃溢出队列的消息。 1737 | 1738 | 简而言之,如果你要使用持久化的订阅者,就必须在发布者端设置阈值,否则可能造成服务器因内存溢出而崩溃。但是,还有另一种方法。ZMQ提供了名为交换区(swap)的机制,它是一个磁盘文件,用于存放从队列中溢出的消息。启动它很简单: 1739 | 1740 | ```c 1741 | // 指定交换区大小,单位:字节。 1742 | uint64_t swap = 25000000; 1743 | zmq_setsockopt (publisher, ZMQ_SWAP, &swap, sizeof (swap)); 1744 | ``` 1745 | 1746 | 我们可以将上面的方法综合起来,编写一个既能接受持久化套接字,又不至于内存溢出的发布者: 1747 | 1748 | **durapub2: Durable but cynical publisher in C** 1749 | 1750 | ```c 1751 | // 1752 | // 发布者 - 连接持久化订阅者 1753 | // 1754 | #include "zhelpers.h" 1755 | 1756 | int main (void) 1757 | { 1758 | void *context = zmq_init (1); 1759 | 1760 | // 订阅者会告知我们它已就绪 1761 | void *sync = zmq_socket (context, ZMQ_PULL); 1762 | zmq_bind (sync, "tcp://*:5564"); 1763 | 1764 | // 使用该套接字发送消息 1765 | void *publisher = zmq_socket (context, ZMQ_PUB); 1766 | 1767 | // 避免慢持久化订阅者消息溢出的问题 1768 | uint64_t hwm = 1; 1769 | zmq_setsockopt (publisher, ZMQ_HWM, &hwm, sizeof (hwm)); 1770 | 1771 | // 设置交换区大小,供所有订阅者使用 1772 | uint64_t swap = 25000000; 1773 | zmq_setsockopt (publisher, ZMQ_SWAP, &swap, sizeof (swap)); 1774 | zmq_bind (publisher, "tcp://*:5565"); 1775 | 1776 | // 等待同步消息 1777 | char *string = s_recv (sync); 1778 | free (string); 1779 | 1780 | // 发布10条消息,一秒一条 1781 | int update_nbr; 1782 | for (update_nbr = 0; update_nbr < 10; update_nbr++) { 1783 | char string [20]; 1784 | sprintf (string, "Update %d", update_nbr); 1785 | s_send (publisher, string); 1786 | sleep (1); 1787 | } 1788 | s_send (publisher, "END"); 1789 | 1790 | zmq_close (sync); 1791 | zmq_close (publisher); 1792 | zmq_term (context); 1793 | return 0; 1794 | } 1795 | ``` 1796 | 1797 | 若在现实环境中将阈值设置为1,致使所有待发送的消息都保存到磁盘上,会大大降低处理速度。这里有一些典型的方法用以处理不同的订阅者: 1798 | 1799 | * **必须为PUB套接字设置阈值**,具体数字可以通过最大订阅者数、可供队列使用的最大内存区域、以及消息的平均大小来衡量。举例来说,你预计会有5000个订阅者,有1G的内存可供使用,消息大小在200个字节左右,那么,一个合理的阈值是1,000,000,000 / 200 / 5,000 = 1,000。 1800 | 1801 | * 如果你不希望慢速或崩溃的订阅者丢失消息,可以设置一个交换区,在高峰期的时候存放这些消息。交换区的大小可以根据订阅者数、高峰消息比率、消息平均大小、暂存时间等来衡量。比如,你预计有5000个订阅者,消息大小为200个字节左右,每秒会有10万条消息。这样,你每秒就需要100MB的磁盘空间来存放消息。加总起来,你会需要6GB的磁盘空间,而且必须足够的快(这超出了本指南的讲解范围)。 1802 | 1803 | 关于持久化订阅者: 1804 | 1805 | * 数据可能会丢失,这要看消息发布的频率、网络缓存大小、通信协议等。持久化的订阅者比起瞬时套接字要可靠一些,但也并不是完美的。 1806 | 1807 | * 交换区文件是无法恢复的,所以当发布者或代理消亡时,交换区中的数据仍然会丢失。 1808 | 1809 | 关于阈值: 1810 | 1811 | * 这个选项会同时影响套接字的发送和接收队列。当然,PUB、PUSH不会有接收队列,SUB、PULL、REQ、REP不会有发送队列。而像DEALER、ROUTER、PAIR套接字时,他们既有发送队列,又有接收队列。 1812 | 1813 | * 当套接字达到阈值时,ZMQ会发生阻塞,或直接丢弃消息。 1814 | 1815 | * 使用inproc协议时,发送者和接受者共享同一个队列缓存,所以说,真正的阈值是两个套接字阈值之和。如果一方套接字没有设置阈值,那么它就不会有缓存方面的限制。 1816 | 1817 | ### 这就是你想要的! 1818 | 1819 | ZMQ就像是一盒积木,只要你有足够的想象力,就可以用它组装出任何造型的网络架构。 1820 | 1821 | 这种高可扩、高弹性的架构一定会打开你的眼界。其实这并不是ZMQ原创的,早就有像[Erlang](http://www.erlang.org/)这样的[基于流的编程语言](http://en.wikipedia.org/wiki/Flow-based_programming)已经能够做到了,只是ZMQ提供了更为友善和易用的接口。 1822 | 1823 | 正如[Gonzo Diethelm](http://permalink.gmane.org/gmane.network.zeromq.devel/2145)所言:“我想用一句话来总结,‘如果ZMQ不存在,那它就应该被发明出来。’作为一个有着多年相关工作经验的人,ZMQ太能引起我的共鸣了。我只能说,‘这就是我想要的!’” 1824 | -------------------------------------------------------------------------------- /images/chapter1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_1.png -------------------------------------------------------------------------------- /images/chapter1_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_2.png -------------------------------------------------------------------------------- /images/chapter1_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_3.png -------------------------------------------------------------------------------- /images/chapter1_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_4.png -------------------------------------------------------------------------------- /images/chapter1_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_5.png -------------------------------------------------------------------------------- /images/chapter1_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_6.png -------------------------------------------------------------------------------- /images/chapter1_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_7.png -------------------------------------------------------------------------------- /images/chapter1_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_8.png -------------------------------------------------------------------------------- /images/chapter1_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter1_9.png -------------------------------------------------------------------------------- /images/chapter2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_1.png -------------------------------------------------------------------------------- /images/chapter2_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_10.png -------------------------------------------------------------------------------- /images/chapter2_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_11.png -------------------------------------------------------------------------------- /images/chapter2_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_12.png -------------------------------------------------------------------------------- /images/chapter2_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_13.png -------------------------------------------------------------------------------- /images/chapter2_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_14.png -------------------------------------------------------------------------------- /images/chapter2_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_15.png -------------------------------------------------------------------------------- /images/chapter2_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_16.png -------------------------------------------------------------------------------- /images/chapter2_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_17.png -------------------------------------------------------------------------------- /images/chapter2_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_18.png -------------------------------------------------------------------------------- /images/chapter2_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_2.png -------------------------------------------------------------------------------- /images/chapter2_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_3.png -------------------------------------------------------------------------------- /images/chapter2_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_4.png -------------------------------------------------------------------------------- /images/chapter2_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_5.png -------------------------------------------------------------------------------- /images/chapter2_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_6.png -------------------------------------------------------------------------------- /images/chapter2_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_7.png -------------------------------------------------------------------------------- /images/chapter2_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_8.png -------------------------------------------------------------------------------- /images/chapter2_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter2_9.png -------------------------------------------------------------------------------- /images/chapter3_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_1.png -------------------------------------------------------------------------------- /images/chapter3_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_10.png -------------------------------------------------------------------------------- /images/chapter3_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_11.png -------------------------------------------------------------------------------- /images/chapter3_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_12.png -------------------------------------------------------------------------------- /images/chapter3_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_13.png -------------------------------------------------------------------------------- /images/chapter3_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_14.png -------------------------------------------------------------------------------- /images/chapter3_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_15.png -------------------------------------------------------------------------------- /images/chapter3_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_16.png -------------------------------------------------------------------------------- /images/chapter3_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_17.png -------------------------------------------------------------------------------- /images/chapter3_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_18.png -------------------------------------------------------------------------------- /images/chapter3_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_19.png -------------------------------------------------------------------------------- /images/chapter3_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_2.png -------------------------------------------------------------------------------- /images/chapter3_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_20.png -------------------------------------------------------------------------------- /images/chapter3_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_21.png -------------------------------------------------------------------------------- /images/chapter3_22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_22.png -------------------------------------------------------------------------------- /images/chapter3_23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_23.png -------------------------------------------------------------------------------- /images/chapter3_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_24.png -------------------------------------------------------------------------------- /images/chapter3_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_25.png -------------------------------------------------------------------------------- /images/chapter3_26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_26.png -------------------------------------------------------------------------------- /images/chapter3_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_3.png -------------------------------------------------------------------------------- /images/chapter3_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_4.png -------------------------------------------------------------------------------- /images/chapter3_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_5.png -------------------------------------------------------------------------------- /images/chapter3_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_6.png -------------------------------------------------------------------------------- /images/chapter3_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_7.png -------------------------------------------------------------------------------- /images/chapter3_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_8.png -------------------------------------------------------------------------------- /images/chapter3_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter3_9.png -------------------------------------------------------------------------------- /images/chapter4_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_1.png -------------------------------------------------------------------------------- /images/chapter4_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_2.png -------------------------------------------------------------------------------- /images/chapter4_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_3.png -------------------------------------------------------------------------------- /images/chapter4_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_4.png -------------------------------------------------------------------------------- /images/chapter4_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_5.png -------------------------------------------------------------------------------- /images/chapter4_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_6.png -------------------------------------------------------------------------------- /images/chapter4_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_7.png -------------------------------------------------------------------------------- /images/chapter4_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_8.png -------------------------------------------------------------------------------- /images/chapter4_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter4_9.png -------------------------------------------------------------------------------- /images/chapter5_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter5_1.png -------------------------------------------------------------------------------- /images/chapter5_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter5_2.png -------------------------------------------------------------------------------- /images/chapter5_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter5_3.png -------------------------------------------------------------------------------- /images/chapter5_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter5_4.png -------------------------------------------------------------------------------- /images/chapter5_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter5_5.png -------------------------------------------------------------------------------- /images/chapter5_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter5_6.png -------------------------------------------------------------------------------- /images/chapter5_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjuke/zguide-cn/91d94b7e87b50787efa481da6a38d313f5888f97/images/chapter5_7.png --------------------------------------------------------------------------------